1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8
9let
10
11 bootFs = lib.filterAttrs (
12 n: fs: (fs.fsType == "bcachefs") && (utils.fsNeededForBoot fs)
13 ) config.fileSystems;
14
15 commonFunctions = ''
16 prompt() {
17 local name="$1"
18 printf "enter passphrase for $name: "
19 }
20
21 tryUnlock() {
22 local name="$1"
23 local path="$2"
24 local success=false
25 local target
26 local uuid=$(echo -n $path | sed -e 's,UUID=\(.*\),\1,g')
27
28 printf "waiting for device to appear $path"
29 for try in $(seq 10); do
30 if [ -e $path ]; then
31 target=$(readlink -f $path)
32 success=true
33 break
34 else
35 target=$(blkid --uuid $uuid)
36 if [ $? == 0 ]; then
37 success=true
38 break
39 fi
40 fi
41 echo -n "."
42 sleep 1
43 done
44 printf "\n"
45 if [ $success == true ]; then
46 path=$target
47 fi
48
49 if bcachefs unlock -c $path > /dev/null 2> /dev/null; then # test for encryption
50 prompt $name
51 until bcachefs unlock $path 2> /dev/null; do # repeat until successfully unlocked
52 printf "unlocking failed!\n"
53 prompt $name
54 done
55 printf "unlocking successful.\n"
56 else
57 echo "Cannot unlock device $uuid with path $path" >&2
58 fi
59 }
60 '';
61
62 # we need only unlock one device manually, and cannot pass multiple at once
63 # remove this adaptation when bcachefs implements mounting by filesystem uuid
64 # also, implement automatic waiting for the constituent devices when that happens
65 # bcachefs does not support mounting devices with colons in the path, ergo we don't (see #49671)
66 firstDevice = fs: lib.head (lib.splitString ":" fs.device);
67
68 useClevis =
69 fs:
70 config.boot.initrd.clevis.enable
71 && (lib.hasAttr (firstDevice fs) config.boot.initrd.clevis.devices);
72
73 openCommand =
74 name: fs:
75 if useClevis fs then
76 ''
77 if clevis decrypt < /etc/clevis/${firstDevice fs}.jwe | bcachefs unlock ${firstDevice fs}
78 then
79 printf "unlocked ${name} using clevis\n"
80 else
81 printf "falling back to interactive unlocking...\n"
82 tryUnlock ${name} ${firstDevice fs}
83 fi
84 ''
85 else
86 ''
87 tryUnlock ${name} ${firstDevice fs}
88 '';
89
90 mkUnits =
91 prefix: name: fs:
92 let
93 mountUnit = "${utils.escapeSystemdPath (prefix + (lib.removeSuffix "/" fs.mountPoint))}.mount";
94 device = firstDevice fs;
95 deviceUnit = "${utils.escapeSystemdPath device}.device";
96 in
97 {
98 name = "unlock-bcachefs-${utils.escapeSystemdPath fs.mountPoint}";
99 value = {
100 description = "Unlock bcachefs for ${fs.mountPoint}";
101 requiredBy = [ mountUnit ];
102 after = [ deviceUnit ];
103 before = [
104 mountUnit
105 "shutdown.target"
106 ];
107 bindsTo = [ deviceUnit ];
108 conflicts = [ "shutdown.target" ];
109 unitConfig.DefaultDependencies = false;
110 serviceConfig = {
111 Type = "oneshot";
112 ExecCondition = "${pkgs.bcachefs-tools}/bin/bcachefs unlock -c \"${device}\"";
113 Restart = "on-failure";
114 RestartMode = "direct";
115 # Ideally, this service would lock the key on stop.
116 # As is, RemainAfterExit doesn't accomplish anything.
117 RemainAfterExit = true;
118 };
119 script =
120 let
121 unlock = ''${pkgs.bcachefs-tools}/bin/bcachefs unlock "${device}"'';
122 unlockInteractively = ''${config.boot.initrd.systemd.package}/bin/systemd-ask-password --timeout=0 "enter passphrase for ${name}" | exec ${unlock}'';
123 in
124 if useClevis fs then
125 ''
126 if ${config.boot.initrd.clevis.package}/bin/clevis decrypt < "/etc/clevis/${device}.jwe" | ${unlock}
127 then
128 printf "unlocked ${name} using clevis\n"
129 else
130 printf "falling back to interactive unlocking...\n"
131 ${unlockInteractively}
132 fi
133 ''
134 else
135 ''
136 ${unlockInteractively}
137 '';
138 };
139 };
140
141 assertions = [
142 {
143 assertion =
144 let
145 kernel = config.boot.kernelPackages.kernel;
146 in
147 (
148 kernel.kernelAtLeast "6.7"
149 || (lib.elem (kernel.structuredExtraConfig.BCACHEFS_FS or null) [
150 lib.kernel.module
151 lib.kernel.yes
152 (lib.kernel.option lib.kernel.yes)
153 ])
154 );
155
156 message = "Linux 6.7-rc1 at minimum or a custom linux kernel with bcachefs support is required";
157 }
158 ];
159in
160
161{
162 config = lib.mkIf (config.boot.supportedFilesystems.bcachefs or false) (
163 lib.mkMerge [
164 {
165 inherit assertions;
166 # needed for systemd-remount-fs
167 system.fsPackages = [ pkgs.bcachefs-tools ];
168 # FIXME: Remove this line when the LTS (default) kernel is at least version 6.7
169 boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
170 services.udev.packages = [ pkgs.bcachefs-tools ];
171
172 systemd = {
173 packages = [ pkgs.bcachefs-tools ];
174 services = lib.mapAttrs' (mkUnits "") (
175 lib.filterAttrs (n: fs: (fs.fsType == "bcachefs") && (!utils.fsNeededForBoot fs)) config.fileSystems
176 );
177 };
178 }
179
180 (lib.mkIf ((config.boot.initrd.supportedFilesystems.bcachefs or false) || (bootFs != { })) {
181 inherit assertions;
182 # chacha20 and poly1305 are required only for decryption attempts
183 boot.initrd.availableKernelModules = [
184 "bcachefs"
185 "sha256"
186 "chacha20"
187 "poly1305"
188 ];
189 boot.initrd.systemd.extraBin = {
190 # do we need this? boot/systemd.nix:566 & boot/systemd/initrd.nix:357
191 "bcachefs" = "${pkgs.bcachefs-tools}/bin/bcachefs";
192 "mount.bcachefs" = "${pkgs.bcachefs-tools}/bin/mount.bcachefs";
193 };
194 boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
195 copy_bin_and_libs ${pkgs.bcachefs-tools}/bin/bcachefs
196 copy_bin_and_libs ${pkgs.bcachefs-tools}/bin/mount.bcachefs
197 '';
198 boot.initrd.extraUtilsCommandsTest = lib.mkIf (!config.boot.initrd.systemd.enable) ''
199 $out/bin/bcachefs version
200 '';
201
202 boot.initrd.postDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) (
203 commonFunctions + lib.concatStrings (lib.mapAttrsToList openCommand bootFs)
204 );
205
206 boot.initrd.systemd.services = lib.mapAttrs' (mkUnits "/sysroot") bootFs;
207 })
208 ]
209 );
210}