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