1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 luks = config.boot.initrd.luks;
7
8 openCommand = name': { name, device, header, keyFile, keyFileSize, allowDiscards, yubikey, fallbackToPassword, ... }: assert name' == name; ''
9
10 # Wait for a target (e.g. device, keyFile, header, ...) to appear.
11 wait_target() {
12 local name="$1"
13 local target="$2"
14
15 if [ ! -e $target ]; then
16 echo -n "Waiting 10 seconds for $name $target to appear"
17 local success=false;
18 for try in $(seq 10); do
19 echo -n "."
20 sleep 1
21 if [ -e $target ]; then success=true break; fi
22 done
23 if [ $success = true ]; then
24 echo " - success";
25 else
26 echo " - failure";
27 fi
28 fi
29 }
30
31 # Wait for luksRoot (and optionally keyFile and/or header) to appear, e.g.
32 # if on a USB drive.
33 wait_target "device" ${device}
34
35 ${optionalString (keyFile != null) ''
36 wait_target "key file" ${keyFile}
37 ''}
38
39 ${optionalString (header != null) ''
40 wait_target "header" ${header}
41 ''}
42
43 open_normally() {
44 echo luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} \
45 ${optionalString (header != null) "--header=${header}"} \
46 > /.luksopen_args
47 ${optionalString (keyFile != null) ''
48 ${optionalString fallbackToPassword "if [ -e ${keyFile} ]; then"}
49 echo " --key-file=${keyFile} ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"}" \
50 >> /.luksopen_args
51 ${optionalString fallbackToPassword ''
52 else
53 echo "keyfile ${keyFile} not found -- fallback to interactive unlocking"
54 fi
55 ''}
56 ''}
57 cryptsetup-askpass
58 rm /.luksopen_args
59 }
60
61 ${optionalString (luks.yubikeySupport && (yubikey != null)) ''
62
63 rbtohex() {
64 ( od -An -vtx1 | tr -d ' \n' )
65 }
66
67 hextorb() {
68 ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI' | xargs printf )
69 }
70
71 open_yubikey() {
72
73 # Make all of these local to this function
74 # to prevent their values being leaked
75 local salt
76 local iterations
77 local k_user
78 local challenge
79 local response
80 local k_luks
81 local opened
82 local new_salt
83 local new_iterations
84 local new_challenge
85 local new_response
86 local new_k_luks
87
88 mkdir -p ${yubikey.storage.mountPoint}
89 mount -t ${yubikey.storage.fsType} ${toString yubikey.storage.device} ${yubikey.storage.mountPoint}
90
91 salt="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path} | sed -n 1p | tr -d '\n')"
92 iterations="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path} | sed -n 2p | tr -d '\n')"
93 challenge="$(echo -n $salt | openssl-wrap dgst -binary -sha512 | rbtohex)"
94 response="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)"
95
96 for try in $(seq 3); do
97
98 ${optionalString yubikey.twoFactor ''
99 echo -n "Enter two-factor passphrase: "
100 read -s k_user
101 echo
102 ''}
103
104 if [ ! -z "$k_user" ]; then
105 k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)"
106 else
107 k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)"
108 fi
109
110 echo -n "$k_luks" | hextorb | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=-
111
112 if [ $? == "0" ]; then
113 opened=true
114 break
115 else
116 opened=false
117 echo "Authentication failed!"
118 fi
119 done
120
121 if [ "$opened" == false ]; then
122 umount ${yubikey.storage.mountPoint}
123 echo "Maximum authentication errors reached"
124 exit 1
125 fi
126
127 echo -n "Gathering entropy for new salt (please enter random keys to generate entropy if this blocks for long)..."
128 for i in $(seq ${toString yubikey.saltLength}); do
129 byte="$(dd if=/dev/random bs=1 count=1 2>/dev/null | rbtohex)";
130 new_salt="$new_salt$byte";
131 echo -n .
132 done;
133 echo "ok"
134
135 new_iterations="$iterations"
136 ${optionalString (yubikey.iterationStep > 0) ''
137 new_iterations="$(($new_iterations + ${toString yubikey.iterationStep}))"
138 ''}
139
140 new_challenge="$(echo -n $new_salt | openssl-wrap dgst -binary -sha512 | rbtohex)"
141
142 new_response="$(ykchalresp -${toString yubikey.slot} -x $new_challenge 2>/dev/null)"
143
144 if [ ! -z "$k_user" ]; then
145 new_k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)"
146 else
147 new_k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)"
148 fi
149
150 mkdir -p ${yubikey.ramfsMountPoint}
151 # A ramfs is used here to ensure that the file used to update
152 # the key slot with cryptsetup will never get swapped out.
153 # Warning: Do NOT replace with tmpfs!
154 mount -t ramfs none ${yubikey.ramfsMountPoint}
155
156 echo -n "$new_k_luks" | hextorb > ${yubikey.ramfsMountPoint}/new_key
157 echo -n "$k_luks" | hextorb | cryptsetup luksChangeKey ${device} --key-file=- ${yubikey.ramfsMountPoint}/new_key
158
159 if [ $? == "0" ]; then
160 echo -ne "$new_salt\n$new_iterations" > ${yubikey.storage.mountPoint}${yubikey.storage.path}
161 else
162 echo "Warning: Could not update LUKS key, current challenge persists!"
163 fi
164
165 rm -f ${yubikey.ramfsMountPoint}/new_key
166 umount ${yubikey.ramfsMountPoint}
167 rm -rf ${yubikey.ramfsMountPoint}
168
169 umount ${yubikey.storage.mountPoint}
170 }
171
172 ${optionalString (yubikey.gracePeriod > 0) ''
173 echo -n "Waiting ${toString yubikey.gracePeriod} seconds as grace..."
174 for i in $(seq ${toString yubikey.gracePeriod}); do
175 sleep 1
176 echo -n .
177 done
178 echo "ok"
179 ''}
180
181 yubikey_missing=true
182 ykinfo -v 1>/dev/null 2>&1
183 if [ $? != "0" ]; then
184 echo -n "waiting 10 seconds for yubikey to appear..."
185 for try in $(seq 10); do
186 sleep 1
187 ykinfo -v 1>/dev/null 2>&1
188 if [ $? == "0" ]; then
189 yubikey_missing=false
190 break
191 fi
192 echo -n .
193 done
194 echo "ok"
195 else
196 yubikey_missing=false
197 fi
198
199 if [ "$yubikey_missing" == true ]; then
200 echo "no yubikey found, falling back to non-yubikey open procedure"
201 open_normally
202 else
203 open_yubikey
204 fi
205 ''}
206
207 # open luksRoot and scan for logical volumes
208 ${optionalString ((!luks.yubikeySupport) || (yubikey == null)) ''
209 open_normally
210 ''}
211 '';
212
213 preLVM = filterAttrs (n: v: v.preLVM) luks.devices;
214 postLVM = filterAttrs (n: v: !v.preLVM) luks.devices;
215
216in
217{
218
219 options = {
220
221 boot.initrd.luks.mitigateDMAAttacks = mkOption {
222 type = types.bool;
223 default = true;
224 description = ''
225 Unless enabled, encryption keys can be easily recovered by an attacker with physical
226 access to any machine with PCMCIA, ExpressCard, ThunderBolt or FireWire port.
227 More information is available at <link xlink:href="http://en.wikipedia.org/wiki/DMA_attack"/>.
228
229 This option blacklists FireWire drivers, but doesn't remove them. You can manually
230 load the drivers if you need to use a FireWire device, but don't forget to unload them!
231 '';
232 };
233
234 boot.initrd.luks.cryptoModules = mkOption {
235 type = types.listOf types.str;
236 default =
237 [ "aes" "aes_generic" "blowfish" "twofish"
238 "serpent" "cbc" "xts" "lrw" "sha1" "sha256" "sha512"
239
240 (if pkgs.stdenv.system == "x86_64-linux" then "aes_x86_64" else "aes_i586")
241 ];
242 description = ''
243 A list of cryptographic kernel modules needed to decrypt the root device(s).
244 The default includes all common modules.
245 '';
246 };
247
248 boot.initrd.luks.forceLuksSupportInInitrd = mkOption {
249 type = types.bool;
250 default = false;
251 internal = true;
252 description = ''
253 Whether to configure luks support in the initrd, when no luks
254 devices are configured.
255 '';
256 };
257
258 boot.initrd.luks.devices = mkOption {
259 default = { };
260 example = { "luksroot".device = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08"; };
261 description = ''
262 The encrypted disk that should be opened before the root
263 filesystem is mounted. Both LVM-over-LUKS and LUKS-over-LVM
264 setups are supported. The unencrypted devices can be accessed as
265 <filename>/dev/mapper/<replaceable>name</replaceable></filename>.
266 '';
267
268 type = with types; loaOf (submodule (
269 { name, ... }: { options = {
270
271 name = mkOption {
272 visible = false;
273 default = name;
274 example = "luksroot";
275 type = types.str;
276 description = "Name of the unencrypted device in <filename>/dev/mapper</filename>.";
277 };
278
279 device = mkOption {
280 example = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08";
281 type = types.str;
282 description = "Path of the underlying encrypted block device.";
283 };
284
285 header = mkOption {
286 default = null;
287 example = "/root/header.img";
288 type = types.nullOr types.str;
289 description = ''
290 The name of the file or block device that
291 should be used as header for the encrypted device.
292 '';
293 };
294
295 keyFile = mkOption {
296 default = null;
297 example = "/dev/sdb1";
298 type = types.nullOr types.str;
299 description = ''
300 The name of the file (can be a raw device or a partition) that
301 should be used as the decryption key for the encrypted device. If
302 not specified, you will be prompted for a passphrase instead.
303 '';
304 };
305
306 keyFileSize = mkOption {
307 default = null;
308 example = 4096;
309 type = types.nullOr types.int;
310 description = ''
311 The size of the key file. Use this if only the beginning of the
312 key file should be used as a key (often the case if a raw device
313 or partition is used as key file). If not specified, the whole
314 <literal>keyFile</literal> will be used decryption, instead of just
315 the first <literal>keyFileSize</literal> bytes.
316 '';
317 };
318
319 # FIXME: get rid of this option.
320 preLVM = mkOption {
321 default = true;
322 type = types.bool;
323 description = "Whether the luksOpen will be attempted before LVM scan or after it.";
324 };
325
326 allowDiscards = mkOption {
327 default = false;
328 type = types.bool;
329 description = ''
330 Whether to allow TRIM requests to the underlying device. This option
331 has security implications; please read the LUKS documentation before
332 activating it.
333 '';
334 };
335
336 fallbackToPassword = mkOption {
337 default = false;
338 type = types.bool;
339 description = ''
340 Whether to fallback to interactive passphrase prompt if the keyfile
341 cannot be found. This will prevent unattended boot should the keyfile
342 go missing.
343 '';
344 };
345
346 yubikey = mkOption {
347 default = null;
348 description = ''
349 The options to use for this LUKS device in Yubikey-PBA.
350 If null (the default), Yubikey-PBA will be disabled for this device.
351 '';
352
353 type = with types; nullOr (submodule {
354 options = {
355 twoFactor = mkOption {
356 default = true;
357 type = types.bool;
358 description = "Whether to use a passphrase and a Yubikey (true), or only a Yubikey (false).";
359 };
360
361 slot = mkOption {
362 default = 2;
363 type = types.int;
364 description = "Which slot on the Yubikey to challenge.";
365 };
366
367 saltLength = mkOption {
368 default = 16;
369 type = types.int;
370 description = "Length of the new salt in byte (64 is the effective maximum).";
371 };
372
373 keyLength = mkOption {
374 default = 64;
375 type = types.int;
376 description = "Length of the LUKS slot key derived with PBKDF2 in byte.";
377 };
378
379 iterationStep = mkOption {
380 default = 0;
381 type = types.int;
382 description = "How much the iteration count for PBKDF2 is increased at each successful authentication.";
383 };
384
385 gracePeriod = mkOption {
386 default = 2;
387 type = types.int;
388 description = "Time in seconds to wait before attempting to find the Yubikey.";
389 };
390
391 ramfsMountPoint = mkOption {
392 default = "/crypt-ramfs";
393 type = types.str;
394 description = "Path where the ramfs used to update the LUKS key will be mounted during early boot.";
395 };
396
397 /* TODO: Add to the documentation of the current module:
398
399 Options related to the storing the salt.
400 */
401 storage = {
402 device = mkOption {
403 default = "/dev/sda1";
404 type = types.path;
405 description = ''
406 An unencrypted device that will temporarily be mounted in stage-1.
407 Must contain the current salt to create the challenge for this LUKS device.
408 '';
409 };
410
411 fsType = mkOption {
412 default = "vfat";
413 type = types.str;
414 description = "The filesystem of the unencrypted device.";
415 };
416
417 mountPoint = mkOption {
418 default = "/crypt-storage";
419 type = types.str;
420 description = "Path where the unencrypted device will be mounted during early boot.";
421 };
422
423 path = mkOption {
424 default = "/crypt-storage/default";
425 type = types.str;
426 description = ''
427 Absolute path of the salt on the unencrypted device with
428 that device's root directory as "/".
429 '';
430 };
431 };
432 };
433 });
434 };
435
436 }; }));
437 };
438
439 boot.initrd.luks.yubikeySupport = mkOption {
440 default = false;
441 type = types.bool;
442 description = ''
443 Enables support for authenticating with a Yubikey on LUKS devices.
444 See the NixOS wiki for information on how to properly setup a LUKS device
445 and a Yubikey to work with this feature.
446 '';
447 };
448 };
449
450 config = mkIf (luks.devices != {} || luks.forceLuksSupportInInitrd) {
451
452 # actually, sbp2 driver is the one enabling the DMA attack, but this needs to be tested
453 boot.blacklistedKernelModules = optionals luks.mitigateDMAAttacks
454 ["firewire_ohci" "firewire_core" "firewire_sbp2"];
455
456 # Some modules that may be needed for mounting anything ciphered
457 # Also load input_leds to get caps lock light working (#12456)
458 boot.initrd.availableKernelModules = [ "dm_mod" "dm_crypt" "cryptd" "input_leds" ]
459 ++ luks.cryptoModules
460 # workaround until https://marc.info/?l=linux-crypto-vger&m=148783562211457&w=4 is merged
461 # remove once 'modprobe --show-depends xts' shows ecb as a dependency
462 ++ (if builtins.elem "xts" luks.cryptoModules then ["ecb"] else []);
463
464 # copy the cryptsetup binary and it's dependencies
465 boot.initrd.extraUtilsCommands = ''
466 copy_bin_and_libs ${pkgs.cryptsetup}/bin/cryptsetup
467
468 cat > $out/bin/cryptsetup-askpass <<EOF
469 #!$out/bin/sh -e
470 if [ -e /.luksopen_args ]; then
471 cryptsetup \$(cat /.luksopen_args)
472 killall -q cryptsetup
473 else
474 echo "Passphrase is not requested now"
475 exit 1
476 fi
477 EOF
478 chmod +x $out/bin/cryptsetup-askpass
479
480 ${optionalString luks.yubikeySupport ''
481 copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykchalresp
482 copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykinfo
483 copy_bin_and_libs ${pkgs.openssl.bin}/bin/openssl
484
485 cc -O3 -I${pkgs.openssl.dev}/include -L${pkgs.openssl.out}/lib ${./pbkdf2-sha512.c} -o pbkdf2-sha512 -lcrypto
486 strip -s pbkdf2-sha512
487 copy_bin_and_libs pbkdf2-sha512
488
489 mkdir -p $out/etc/ssl
490 cp -pdv ${pkgs.openssl.out}/etc/ssl/openssl.cnf $out/etc/ssl
491
492 cat > $out/bin/openssl-wrap <<EOF
493 #!$out/bin/sh
494 export OPENSSL_CONF=$out/etc/ssl/openssl.cnf
495 $out/bin/openssl "\$@"
496 EOF
497 chmod +x $out/bin/openssl-wrap
498 ''}
499 '';
500
501 boot.initrd.extraUtilsCommandsTest = ''
502 $out/bin/cryptsetup --version
503 ${optionalString luks.yubikeySupport ''
504 $out/bin/ykchalresp -V
505 $out/bin/ykinfo -V
506 $out/bin/openssl-wrap version
507 ''}
508 '';
509
510 boot.initrd.preLVMCommands = concatStrings (mapAttrsToList openCommand preLVM);
511 boot.initrd.postDeviceCommands = concatStrings (mapAttrsToList openCommand postLVM);
512
513 environment.systemPackages = [ pkgs.cryptsetup ];
514 };
515}