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