From a9d69a74d6edb6bcca29b1189d4bc3b203ecaf25 Mon Sep 17 00:00:00 2001 From: Jan Malakhovski Date: Fri, 15 Sep 2017 21:48:51 +0000 Subject: nixos: initrd/luks: change passphrases handling Also reuse common cryptsetup invocation subexpressions. - Passphrase reading is done via the shell now, not by cryptsetup. This way the same passphrase can be reused between cryptsetup invocations, which this module now tries to do by default (can be disabled). - Number of retries is now infinity, it makes no sense to make users reboot when they fail to type in their passphrase. --- nixos/modules/system/boot/luksroot.nix | 175 ++++++++++++++++++++++++--------- 1 file changed, 130 insertions(+), 45 deletions(-) diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index 8cdb3981d534..ea6d189d9907 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -67,7 +67,25 @@ let } ''; - openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, fallbackToPassword, ... }: assert name' == name; '' + preCommands = '' + # A place to store crypto things + + # A ramfs is used here to ensure that the file used to update + # the key slot with cryptsetup will never get swapped out. + # Warning: Do NOT replace with tmpfs! + mkdir -p /crypt-ramfs + mount -t ramfs none /crypt-ramfs + ''; + + postCommands = '' + umount /crypt-ramfs 2>/dev/null + ''; + + openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, fallbackToPassword, ... }: assert name' == name; + let + csopen = "cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} ${optionalString (header != null) "--header=${header}"}"; + cschange = "cryptsetup luksChangeKey ${device} ${optionalString (header != null) "--header=${header}"}"; + in '' # Wait for luksRoot (and optionally keyFile and/or header) to appear, e.g. # if on a USB drive. wait_target "device" ${device} || die "${device} is unavailable" @@ -76,31 +94,72 @@ let wait_target "header" ${header} || die "${header} is unavailable" ''} - ${optionalString (keyFile != null) '' - wait_target "key file" ${keyFile} || die "${keyFile} is unavailable" - ''} + do_open_passphrase() { + local passphrase + + while true; do + echo -n "Passphrase for ${device}: " + passphrase= + while true; do + if [ -e /crypt-ramfs/passphrase ]; then + echo "reused" + passphrase=$(cat /crypt-ramfs/passphrase) + break + else + # ask cryptsetup-askpass + echo -n "${device}" > /crypt-ramfs/device + + # and try reading it from /dev/console + IFS= read -t 1 -rs passphrase + if [ -n "$passphrase" ]; then + ${if luks.reusePassphrases then '' + # remember it for the next device + echo -n "$passphrase" > /crypt-ramfs/passphrase + '' else '' + # Don't save it to ramfs. We are very paranoid + ''} + echo + break + fi + fi + done + echo -n "Verifiying passphrase for ${device}..." + echo -n "$passphrase" | ${csopen} --key-file=- + if [ $? == 0 ]; then + echo " - success" + ${if luks.reusePassphrases then '' + # we don't rm here because we might reuse it for the next device + '' else '' + rm -f /crypt-ramfs/passphrase + ''} + break + else + echo " - failure" + # ask for a different one + rm -f /crypt-ramfs/passphrase + fi + done + } + # LUKS open_normally() { - echo luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} \ - ${optionalString (header != null) "--header=${header}"} \ - > /.luksopen_args - ${optionalString (keyFile != null) '' - ${optionalString fallbackToPassword "if [ -e ${keyFile} ]; then"} - echo " --key-file=${keyFile} ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"}" \ - "${optionalString (keyFileOffset != null) "--keyfile-offset=${toString keyFileOffset}"}" \ - >> /.luksopen_args - ${optionalString fallbackToPassword '' + ${if (keyFile != null) then '' + if wait_target "key file" ${keyFile}; then + ${csopen} --key-file=${keyFile} \ + ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"} \ + ${optionalString (keyFileOffset != null) "--keyfile-offset=${toString keyFileOffset}"} else - echo "keyfile ${keyFile} not found -- fallback to interactive unlocking" + ${if fallbackToPassword then "echo" else "die"} "${keyFile} is unavailable" + echo " - failing back to interactive password prompt" + do_open_passphrase fi + '' else '' + do_open_passphrase ''} - ''} - cryptsetup-askpass - rm /.luksopen_args } - ${optionalString (luks.yubikeySupport && (yubikey != null)) '' - + ${if luks.yubikeySupport && (yubikey != null) then '' + # Yubikey rbtohex() { ( od -An -vtx1 | tr -d ' \n' ) } @@ -109,7 +168,7 @@ let ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI' | xargs printf ) } - open_yubikey() { + do_open_yubikey() { # Make all of these local to this function # to prevent their values being leaked local salt @@ -146,7 +205,7 @@ let k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)" fi - echo -n "$k_luks" | hextorb | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=- + echo -n "$k_luks" | hextorb | ${csopen} --key-file=- if [ $? == 0 ]; then opened=true @@ -192,7 +251,7 @@ let mount -t ramfs none ${yubikey.ramfsMountPoint} echo -n "$new_k_luks" | hextorb > ${yubikey.ramfsMountPoint}/new_key - echo -n "$k_luks" | hextorb | cryptsetup luksChangeKey ${device} --key-file=- ${yubikey.ramfsMountPoint}/new_key + echo -n "$k_luks" | hextorb | ${cschange} --key-file=- ${yubikey.ramfsMountPoint}/new_key if [ $? == 0 ]; then echo -ne "$new_salt\n$new_iterations" > ${yubikey.storage.mountPoint}${yubikey.storage.path} @@ -207,20 +266,39 @@ let umount ${yubikey.storage.mountPoint} } - if wait_yubikey ${toString yubikey.gracePeriod}; then - open_yubikey - else - echo "no yubikey found, falling back to non-yubikey open procedure" - open_normally - fi - ''} + open_yubikey() { + if wait_yubikey ${toString yubikey.gracePeriod}; then + do_open_yubikey + else + echo "No yubikey found, falling back to non-yubikey open procedure" + open_normally + fi + } - # open luksRoot and scan for logical volumes - ${optionalString ((!luks.yubikeySupport) || (yubikey == null)) '' + open_yubikey + '' else '' open_normally ''} ''; + askPass = pkgs.writeScriptBin "cryptsetup-askpass" '' + #!/bin/sh + + ${commonFunctions} + + while true; do + wait_target "luks" /crypt-ramfs/device 10 "LUKS to request a passphrase" || die "Passphrase is not requested now" + device=$(cat /crypt-ramfs/device) + + echo -n "Passphrase for $device: " + IFS= read -rs passphrase + echo + + rm /crypt-ramfs/device + echo -n "$passphrase" > /crypt-ramfs/passphrase + done + ''; + preLVM = filterAttrs (n: v: v.preLVM) luks.devices; postLVM = filterAttrs (n: v: !v.preLVM) luks.devices; @@ -266,6 +344,22 @@ in ''; }; + boot.initrd.luks.reusePassphrases = mkOption { + type = types.bool; + default = true; + description = '' + When opening a new LUKS device try reusing last successful + passphrase. + + Useful for mounting a number of devices that use the same + passphrase without retyping it several times. + + Such setup can be useful if you use cryptsetup + luksSuspend. Different LUKS devices will still have + different master keys even when using the same passphrase. + ''; + }; + boot.initrd.luks.devices = mkOption { default = { }; example = { "luksroot".device = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08"; }; @@ -487,18 +581,8 @@ in # copy the cryptsetup binary and it's dependencies boot.initrd.extraUtilsCommands = '' copy_bin_and_libs ${pkgs.cryptsetup}/bin/cryptsetup - - cat > $out/bin/cryptsetup-askpass <