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