diff --git a/nixos/doc/manual/release-notes/rl-1809.xml b/nixos/doc/manual/release-notes/rl-1809.xml index a70fb1aedbb86b..d527984f5ef142 100644 --- a/nixos/doc/manual/release-notes/rl-1809.xml +++ b/nixos/doc/manual/release-notes/rl-1809.xml @@ -190,6 +190,16 @@ $ nix-instantiate -E '(import <nixpkgsunstable> {}).gitFull' which indicates that the nix output hash will be used as tag. + + + Options + boot.initrd.luks.devices.name.yubikey.ramfsMountPoint + boot.initrd.luks.devices.name.yubikey.storage.mountPoint + were removed. luksroot.nix module never supported more than one YubiKey at + a time anyway, hence those options never had any effect. You should be able to remove them + from your config without any issues. + + diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index 401c3cc69919f9..27c1f891f485a2 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -5,62 +5,171 @@ with lib; let luks = config.boot.initrd.luks; - openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, fallbackToPassword, ... }: assert name' == name; '' + commonFunctions = '' + die() { + echo "$@" >&2 + exit 1 + } - # Wait for a target (e.g. device, keyFile, header, ...) to appear. wait_target() { local name="$1" local target="$2" + local secs="''${3:-10}" + local desc="''${4:-$name $target to appear}" if [ ! -e $target ]; then - echo -n "Waiting 10 seconds for $name $target to appear" + echo -n "Waiting $secs seconds for $desc..." local success=false; - for try in $(seq 10); do + for try in $(seq $secs); do echo -n "." sleep 1 - if [ -e $target ]; then success=true break; fi + if [ -e $target ]; then + success=true + break + fi done - if [ $success = true ]; then + if [ $success == true ]; then echo " - success"; + return 0 else echo " - failure"; + return 1 fi fi + return 0 + } + + wait_yubikey() { + local secs="''${1:-10}" + + ykinfo -v 1>/dev/null 2>&1 + if [ $? != 0 ]; then + echo -n "Waiting $secs seconds for Yubikey to appear..." + local success=false + for try in $(seq $secs); do + echo -n . + sleep 1 + ykinfo -v 1>/dev/null 2>&1 + if [ $? == 0 ]; then + success=true + break + fi + done + if [ $success == true ]; then + echo " - success"; + return 0 + else + echo " - failure"; + return 1 + fi + fi + return 0 } + ''; + + 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 + + # For Yubikey salt storage + mkdir -p /crypt-storage + # Disable all input echo for the whole stage. We could use read -s + # instead but that would ocasionally leak characters between read + # invocations. + stty -echo + ''; + + postCommands = '' + stty echo + umount /crypt-storage 2>/dev/null + 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} - - ${optionalString (keyFile != null) '' - wait_target "key file" ${keyFile} - ''} + wait_target "device" ${device} || die "${device} is unavailable" ${optionalString (header != null) '' - wait_target "header" ${header} + wait_target "header" ${header} || die "${header} 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 with a timeout + IFS= read -t 1 -r 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' ) } @@ -69,8 +178,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 @@ -86,19 +194,18 @@ let local new_response local new_k_luks - mkdir -p ${yubikey.storage.mountPoint} - mount -t ${yubikey.storage.fsType} ${toString yubikey.storage.device} ${yubikey.storage.mountPoint} + mount -t ${yubikey.storage.fsType} ${yubikey.storage.device} /crypt-storage || \ + die "Failed to mount Yubikey salt storage device" - salt="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path} | sed -n 1p | tr -d '\n')" - iterations="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path} | sed -n 2p | tr -d '\n')" + salt="$(cat /crypt-storage${yubikey.storage.path} | sed -n 1p | tr -d '\n')" + iterations="$(cat /crypt-storage${yubikey.storage.path} | sed -n 2p | tr -d '\n')" challenge="$(echo -n $salt | openssl-wrap dgst -binary -sha512 | rbtohex)" response="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)" for try in $(seq 3); do - ${optionalString yubikey.twoFactor '' echo -n "Enter two-factor passphrase: " - read -s k_user + read -r k_user echo ''} @@ -108,9 +215,9 @@ 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 + if [ $? == 0 ]; then opened=true break else @@ -119,11 +226,7 @@ let fi done - if [ "$opened" == false ]; then - umount ${yubikey.storage.mountPoint} - echo "Maximum authentication errors reached" - exit 1 - fi + [ "$opened" == false ] && die "Maximum authentication errors reached" echo -n "Gathering entropy for new salt (please enter random keys to generate entropy if this blocks for long)..." for i in $(seq ${toString yubikey.saltLength}); do @@ -148,67 +251,50 @@ let new_k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)" fi - mkdir -p ${yubikey.ramfsMountPoint} - # 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! - 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 "$new_k_luks" | hextorb > /crypt-ramfs/new_key + echo -n "$k_luks" | hextorb | ${cschange} --key-file=- /crypt-ramfs/new_key - if [ $? == "0" ]; then - echo -ne "$new_salt\n$new_iterations" > ${yubikey.storage.mountPoint}${yubikey.storage.path} + if [ $? == 0 ]; then + echo -ne "$new_salt\n$new_iterations" > /crypt-storage${yubikey.storage.path} else echo "Warning: Could not update LUKS key, current challenge persists!" fi - rm -f ${yubikey.ramfsMountPoint}/new_key - umount ${yubikey.ramfsMountPoint} - rm -rf ${yubikey.ramfsMountPoint} + rm -f /crypt-ramfs/new_key + umount /crypt-storage + } - umount ${yubikey.storage.mountPoint} + 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 } - ${optionalString (yubikey.gracePeriod > 0) '' - echo -n "Waiting ${toString yubikey.gracePeriod} seconds as grace..." - for i in $(seq ${toString yubikey.gracePeriod}); do - sleep 1 - echo -n . - done - echo "ok" + open_yubikey + '' else '' + open_normally ''} + ''; - yubikey_missing=true - ykinfo -v 1>/dev/null 2>&1 - if [ $? != "0" ]; then - echo -n "waiting 10 seconds for yubikey to appear..." - for try in $(seq 10); do - sleep 1 - ykinfo -v 1>/dev/null 2>&1 - if [ $? == "0" ]; then - yubikey_missing=false - break - fi - echo -n . - done - echo "ok" - else - yubikey_missing=false - fi - - if [ "$yubikey_missing" == true ]; then - echo "no yubikey found, falling back to non-yubikey open procedure" - open_normally - else - open_yubikey - fi - ''} + askPass = pkgs.writeScriptBin "cryptsetup-askpass" '' + #!/bin/sh - # open luksRoot and scan for logical volumes - ${optionalString ((!luks.yubikeySupport) || (yubikey == null)) '' - open_normally - ''} + ${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; @@ -256,6 +342,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"; }; @@ -397,15 +499,9 @@ in }; gracePeriod = mkOption { - default = 2; + default = 10; type = types.int; - description = "Time in seconds to wait before attempting to find the Yubikey."; - }; - - ramfsMountPoint = mkOption { - default = "/crypt-ramfs"; - type = types.str; - description = "Path where the ramfs used to update the LUKS key will be mounted during early boot."; + description = "Time in seconds to wait for the Yubikey."; }; /* TODO: Add to the documentation of the current module: @@ -428,12 +524,6 @@ in description = "The filesystem of the unencrypted device."; }; - mountPoint = mkOption { - default = "/crypt-storage"; - type = types.str; - description = "Path where the unencrypted device will be mounted during early boot."; - }; - path = mkOption { default = "/crypt-storage/default"; type = types.str; @@ -446,8 +536,8 @@ in }; }); }; - - }; })); + }; + })); }; boot.initrd.luks.yubikeySupport = mkOption { @@ -477,18 +567,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 <