diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index 8547682284f7..117c526fcd38 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -39,153 +39,123 @@ let ${optionalString (luks.yubikeySupport && (yubikey != null)) '' rbtohex() { - od -An -vtx1 | tr -d ' \n' + ( od -An -vtx1 | tr -d ' \n' ) } hextorb() { - tr '[:lower:]' '[:upper:]' | sed -e 's|\([0-9A-F]\{2\}\)|\\\\\\x\1|gI' | xargs printf - } - - take() { - local c="$1" - shift - head -c $c "$@" - } - - drop() { - local c="$1" - shift - if [ -e "$1" ]; then - cat "$1" | ( dd of=/dev/null bs="$c" count=1 2>/dev/null ; dd 2>/dev/null ) - else - ( dd of=/dev/null bs="$c" count=1 2>/dev/null ; dd 2>/dev/null ) - fi + ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI' | xargs printf ) } open_yubikey() { + # Make all of these local to this function + # to prevent their values being leaked + local salt + local iterations + local k_user + local challenge + local response + local k_luks + local opened + local new_salt + local new_iterations + local new_challenge + local new_response + local new_k_luks + mkdir -p ${yubikey.storage.mountPoint} mount -t ${yubikey.storage.fsType} ${toString yubikey.storage.device} ${yubikey.storage.mountPoint} - local uuid_r - local k_user - local challenge - local k_blob - local aes_blob_decrypted - local checksum_correct - local checksum - local uuid_luks - local user_record - - uuid_luks="$(cryptsetup luksUUID ${device} | take 36 | tr -d '-')" - - ${optionalString (!yubikey.multiUser) '' - user_record="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path})" - uuid_r="$(echo -n $user_record | take 32)" - ''} + 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')" + 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.multiUser '' - local user_id - echo -n "Enter user id: " - read -s user_id - echo - ''} - ${optionalString yubikey.twoFactor '' echo -n "Enter two-factor passphrase: " read -s k_user echo ''} - ${optionalString yubikey.multiUser '' - local user_id_hash - user_id_hash="$(echo -n $user_id | openssl-wrap dgst -binary -sha512 | rbtohex)" - - user_record="$(sed -n -e /^$user_id_hash[^$]*$/p ${yubikey.storage.mountPoint}${yubikey.storage.path} | tr -d '\n')" - - if [ ! -z "$user_record" ]; then - user_record="$(echo -n $user_record | drop 128)" - uuid_r="$(echo -n $user_record | take 32)" - ''} - - challenge="$(echo -n $k_user$uuid_r$uuid_luks | openssl-wrap dgst -binary -sha1 | rbtohex)" - - k_blob="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)" - - aes_blob_decrypted="$(echo -n $user_record | drop 32 | hextorb | openssl-wrap enc -d -aes-256-ctr -K $k_blob -iv $uuid_r | rbtohex)" - - checksum="$(echo -n $aes_blob_decrypted | drop 168)" - if [ "$(echo -n $aes_blob_decrypted | hextorb | take 84 | openssl-wrap dgst -binary -sha512 | rbtohex)" == "$checksum" ]; then - checksum_correct=1 - break - else - checksum_correct=0 - echo "Authentication failed!" - fi - - ${optionalString yubikey.multiUser '' + if [ ! -z "$k_user" ]; then + k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)" else - checksum_correct=0 + 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=- + + if [ $? == "0" ]; then + opened=true + break + else + opened=false echo "Authentication failed!" fi - ''} done - if [ "$checksum_correct" != "1" ]; then + if [ "$opened" == false ]; then umount ${yubikey.storage.mountPoint} echo "Maximum authentication errors reached" exit 1 fi - local k_yubi - k_yubi="$(echo -n $aes_blob_decrypted | take 40)" + 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 + byte="$(dd if=/dev/random bs=1 count=1 2>/dev/null | rbtohex)"; + new_salt="$new_salt$byte"; + echo -n . + done; + echo "ok" - local k_luks - k_luks="$(echo -n $aes_blob_decrypted | drop 40 | take 128)" + new_iterations="$iterations" + ${optionalString (yubikey.iterationStep > 0) '' + new_iterations="$(($new_iterations + ${toString yubikey.iterationStep}))" + ''} - echo -n "$k_luks" | hextorb | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=- + new_challenge="$(echo -n $new_salt | openssl-wrap dgst -binary -sha512 | rbtohex)" - update_failed=false + new_response="$(ykchalresp -${toString yubikey.slot} -x $new_challenge 2>/dev/null)" - local new_uuid_r - new_uuid_r="$(uuidgen)" - if [ $? != "0" ]; then - for try in $(seq 10); do - sleep 1 - new_uuid_r="$(uuidgen)" - if [ $? == "0" ]; then break; fi - if [ $try -eq 10 ]; then update_failed=true; fi - done - fi - - if [ "$update_failed" == false ]; then - new_uuid_r="$(echo -n $new_uuid_r | take 36 | tr -d '-')" - - local new_challenge - new_challenge="$(echo -n $k_user$new_uuid_r$uuid_luks | openssl-wrap dgst -binary -sha1 | rbtohex)" - - local new_k_blob - new_k_blob="$(echo -n $new_challenge | hextorb | openssl-wrap dgst -binary -sha1 -mac HMAC -macopt hexkey:$k_yubi | rbtohex)" - - local new_aes_blob - new_aes_blob=$(echo -n "$k_yubi$k_luks$checksum" | hextorb | openssl-wrap enc -e -aes-256-ctr -K "$new_k_blob" -iv "$new_uuid_r" | rbtohex) - - ${optionalString yubikey.multiUser '' - sed -i -e "s|^$user_id_hash$user_record|$user_id_hash$new_uuid_r$new_aes_blob|1" - ''} - - ${optionalString (!yubikey.multiUser) '' - echo -n "$new_uuid_r$new_aes_blob" > ${yubikey.storage.mountPoint}${yubikey.storage.path} - ''} + if [ ! -z "$k_user" ]; then + new_k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)" else - echo "Warning: Could not obtain new UUID, current challenge persists!" + 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 + + if [ $? == "0" ]; then + echo -ne "$new_salt\n$new_iterations" > ${yubikey.storage.mountPoint}${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} + umount ${yubikey.storage.mountPoint} } + ${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" + ''} + yubikey_missing=true ykinfo -v 1>/dev/null 2>&1 if [ $? != "0" ]; then @@ -336,21 +306,45 @@ in description = "Whether to use a passphrase and a Yubikey (true), or only a Yubikey (false)"; }; - multiUser = mkOption { - default = false; - type = types.bool; - description = "Whether to allow multiple users to authenticate with a Yubikey"; - }; - slot = mkOption { default = 2; type = types.int; description = "Which slot on the Yubikey to challenge"; }; + saltLength = mkOption { + default = 16; + type = types.int; + description = "Length of the new salt in byte (64 is the effective maximum)"; + }; + + keyLength = mkOption { + default = 64; + type = types.int; + description = "Length of the LUKS slot key derived with PBKDF2 in byte"; + }; + + iterationStep = mkOption { + default = 0; + type = types.int; + description = "How much the iteration count for PBKDF2 is increased at each successful authentication"; + }; + + gracePeriod = mkOption { + default = 2; + type = types.int; + description = "Time in seconds to wait before attempting to find the Yubikey"; + }; + + ramfsMountPoint = mkOption { + default = "/crypt-ramfs"; + type = types.string; + description = "Path where the ramfs used to update the LUKS key will be mounted in stage-1"; + }; + storage = mkOption { type = types.optionSet; - description = "Options related to the authentication record"; + description = "Options related to the storing the salt"; options = { device = mkOption { @@ -358,7 +352,7 @@ in type = types.path; description = '' An unencrypted device that will temporarily be mounted in stage-1. - Must contain the authentication record for this LUKS device. + Must contain the current salt to create the challenge for this LUKS device. ''; }; @@ -378,7 +372,7 @@ in default = "/crypt-storage/default"; type = types.string; description = '' - Absolute path of the authentication record on the unencrypted device with + Absolute path of the salt on the unencrypted device with that device's root directory as "/". ''; }; @@ -420,11 +414,13 @@ in cp -pdv ${pkgs.popt}/lib/libpopt*.so.* $out/lib ${optionalString luks.yubikeySupport '' - cp -pdv ${pkgs.utillinux}/bin/uuidgen $out/bin cp -pdv ${pkgs.ykpers}/bin/ykchalresp $out/bin cp -pdv ${pkgs.ykpers}/bin/ykinfo $out/bin cp -pdv ${pkgs.openssl}/bin/openssl $out/bin + cc -O3 -I${pkgs.openssl}/include -L${pkgs.openssl}/lib ${./pbkdf2-sha512.c} -o $out/bin/pbkdf2-sha512 -lcrypto + strip -s $out/bin/pbkdf2-sha512 + cp -pdv ${pkgs.libusb1}/lib/libusb*.so.* $out/lib cp -pdv ${pkgs.ykpers}/lib/libykpers*.so.* $out/lib cp -pdv ${pkgs.libyubikey}/lib/libyubikey*.so.* $out/lib @@ -444,7 +440,6 @@ EOF boot.initrd.extraUtilsCommandsTest = '' $out/bin/cryptsetup --version ${optionalString luks.yubikeySupport '' - $out/bin/uuidgen --version $out/bin/ykchalresp -V $out/bin/ykinfo -V cat > $out/bin/openssl-wrap < +#include +#include +#include + +void hextorb(uint8_t* hex, uint8_t* rb) +{ + while(sscanf(hex, "%2x", rb) == 1) + { + hex += 2; + rb += 1; + } + *rb = '\0'; +} + +int main(int argc, char** argv) +{ + uint8_t k_user[2048]; + uint8_t salt[2048]; + uint8_t key[4096]; + + uint32_t key_length = atoi(argv[1]); + uint32_t iteration_count = atoi(argv[2]); + + hextorb(argv[3], salt); + uint32_t salt_length = strlen(argv[3]) / 2; + + fgets(k_user, 2048, stdin); + uint32_t k_user_length = strlen(k_user); + if(k_user[k_user_length - 1] == '\n') { + k_user[k_user_length - 1] = '\0'; + } + + PKCS5_PBKDF2_HMAC(k_user, k_user_length, salt, salt_length, iteration_count, EVP_sha512(), key_length, key); + fwrite(key, 1, key_length, stdout); + + return 0; +} \ No newline at end of file