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