如何从USB加载LUKS密码,回退到键盘



我想设置一台具有全磁盘加密的无头Linux(Debian Wheezy)PC,能够使用USB驱动器或通过键盘输入密码来解锁磁盘。 我的出发点是使用 Debian 安装程序中的基本全磁盘加密选项进行全新安装,该选项将除/boot 以外的所有内容作为 LUKS 加密的逻辑卷组进行管理,并为我提供了键盘选项。 我将在答案中描述我当前的解决方案,希望它会有用,并且其他人可以改进它。

以下是我遇到的一些问题:

  • 设置密码并将其放在 USB 驱动器上。

  • 及时加载 USB 模块。

  • 等待 Linux 识别 USB 驱动器,然后再尝试从中读取。

  • 识别正确的 USB 驱动器(而不是碰巧插入的其他驱动器)。

  • 编写"键脚本"以从 USB 驱动器中提取密码。

  • 确保在所有 USB 故障情况下都启动键盘回退。

我将接受一个有重大改进的答案,并对提供贡献的答案投赞成票。

我的很多解决方案都来自这篇文章,使用 USB 密钥作为 LUKS 密码短语。

  1. 创建随机密码:

     dd if=/dev/urandom bs=1 count=256 > passphrase
    
  2. 插入 U 盘。 dmesg输出将显示设备名称;假设/dev/sdd . 计算其大小:

     blockdev --getsize64 /dev/sdd
    
  3. 我决定在原始设备的末尾安装密码短语,认为它可能会在任何意外使用USB驱动器的情况下幸存下来。

     dd if=passphrase of=/dev/sdd bs=1 seek=<size-256>
    
  4. 将密码添加到 LUKS 卷:

     cryptsetup luksAddKey /dev/sda5 passphrase
    

    这不会影响安装程序中现有的手动输入的密码。 可以删除密码文件:

     rm passphrase
    
  5. 为 U 盘找到一个唯一的名称,以便我们可以在存在时识别它:

     ls -l /dev/disk/by-id | grep -w sdd
    

    您应该看到一个符号链接。 我称之为/dev/disk/by-id/<ID>.

  6. 编辑/etc/crypttab . 您应该看到如下行:

     sdc5_crypt UUID=b9570e0f-3bd3-40b0-801f-ee20ac460207 none luks
    

    将其修改为:

     sdc5_crypt UUID=b9570e0f-3bd3-40b0-801f-ee20ac460207 /dev/disk/by-id/<ID> luks,keyscript=/bin/passphrase-from-usb
    
  7. 上面提到的keyscript需要从 USB 设备读取密码。 但是,它需要做的远不止这些。 要了解如何使用它,请检查 /usr/share/initramfs-tools/scripts/local-top/cryptroot ,在引导时运行的脚本以解锁根设备。 请注意,当设置keyscript时,它只是运行,输出通过管道传输到luksOpen,无需其他检查。 无法发出错误信号(USB 驱动器不存在)或回退到键盘输入。 如果密码失败,则在循环中再次运行密钥脚本,最多运行几次;但是,我们没有被告知我们正在进行哪个迭代。 此外,我们无法控制键盘脚本的运行时间,因此我们无法确定 Linux 是否已识别 USB 驱动器。

    我用一些技巧解决了这个问题:

    1. 在 USB 驱动器上轮询并等待 3 秒钟以使其显示。 这对我有用,但我很想知道更好的方法。

    2. 创建一个虚拟文件/passphrase-from-usb-tried首次运行时,以指示我们至少运行过一次。

    3. 如果我们至少运行过一次,或者找不到 USB 驱动器,请运行cryptroot用于键盘输入的askpass程序。

    最终脚本:

    #!/bin/sh
    set -e
    if ! [ -e /passphrase-from-usb-tried ]; then
        touch /passphrase-from-usb-tried
        if ! [ -e "$CRYPTTAB_KEY" ]; then
            echo "Waiting for USB stick to be recognized..." >&2
            sleep 3
        fi
        if [ -e "$CRYPTTAB_KEY" ]; then
            echo "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME) from USB key" >&2
            dd if="$CRYPTTAB_KEY" bs=1 skip=129498880 count=256 2>/dev/null
            exit
        else
            echo "Can't find $CRYPTTAB_KEY; USB stick not present?" >&2
        fi
    fi
    /lib/cryptsetup/askpass "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME)nEnter passphrase: "
    

    最后,我们需要确保此脚本在 initramfs 中可用。 创建包含以下内容的/etc/initramfs-tools/hooks/passphrase-from-usb

    #!/bin/sh
    PREREQ=""
    prereqs() {
            echo "$PREREQ"
    }
    case "$1" in
            prereqs)
                    prereqs
                    exit 0
            ;;
    esac
    . "${CONFDIR}/initramfs.conf"
    . /usr/share/initramfs-tools/hook-functions
    copy_exec /bin/passphrase-from-usb /bin
    
  8. USB驱动程序不存在于我的初始化中。 (在更高版本的 Debian 中,它们似乎是默认的。 我必须通过添加到/etc/initramfs-tools/modules来添加它们:

     uhci_hcd
     ehci_hcd
     usb_storage
    
  9. 完成所有操作后,更新 initramfs:

     update-initramfs -u
    

如果我能简单地拥有一个包含密码的小 U 盘,那对我来说将是理想的解锁磁盘。这不仅对服务器很方便(您可以将 U 盘留在服务器 - 目标是能够归还损坏的硬盘而不必担心机密数据),这对我的笔记本电脑也很棒:启动时插入 U 盘并在启动后将其取出解锁加密磁盘。

我现在已经编写了一个补丁,它将在所有设备的根目录中搜索文件"cryptkey.txt"并尝试解密以每行为键。如果失败:恢复为键入密码短语。

这确实意味着键不能包含,但这也适用于任何键入的键。好的部分是您可以使用相同的USB磁盘来存储多台机器的密钥:您不需要每台机器的单独USB磁盘。因此,如果您的物理密钥环中有 USB 驱动器,则可以在物理关闭时对启动的所有计算机使用相同的驱动器。

您可以使用以下命令添加密钥:

cryptsetup luksAddKey /dev/sda5

然后将相同的密钥与USB/MMC磁盘上名为"cryptkey.txt的文件中的一行相同。补丁在这里:

https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=864647

如果您的 initramfs 中不存在 USB 驱动程序、MMC 驱动程序或文件系统,则需要通过添加到/etc/initramfs-tools/modules 来添加它们:

uhci_hcd
ehci_hcd
usb_storage
nls_utf8
nls_cp437
vfat
fat
sd_mod
mmc_block
tifm_sd
tifm_core
mmc_core
tifm_7xx1
sdhci
sdhci_pci

完成所有操作后,更新 initramfs:

update-initramfs -u

它可以在以下位置找到补丁和文件: https://gitlab.com/ole.tange/tangetools/tree/master/decrypt-root-with-usb

尽管@Andrew在以前的版本中有效的答案很好。该解决方案实际上已经过时,需要对 ubuntu 18.04 和 19.10 进行大量调整。所以我想分享我对此的研究。

关于crypttab有几个问题。从14.04到18.04和19.10,sepcs实际上发生了很大变化。它开始支持更多加密设置的参数。例如密钥文件偏移量、密钥文件大小等。一些选项,例如nobootwait已经消失了。其他发行版已经支持某些参数,但在 ubuntu 中尚不支持(例如非常好的参数密钥文件超时。这可以消除整个键盘脚本,因为它将在键盘文件超时后自动回退到键盘输入。

在 ubuntu 上 crypttab 的主要陷阱是它实际上由 2 个不同的进程处理。一个是传统的 initramfs,另一个是现代系统。Systemd应该在许多方面更加先进和灵活。但是,systemd 对 crypptab 的支持很差,有很多选项,例如 keyscript 只是默默忽略了。 所以我不知道发生了什么,直到我发现这篇文章。几乎所有关于crypttab设置的在线帖子都是针对initramfs而不是systemd的。因此,我们需要在crypttab中的所有条目中添加initramfs以避免出现问题。

我还发现了一种无需虚拟机或反复重新启动即可调试我们的键盘脚本和密码选项卡的好方法。这是cryptdisks_start.在我们实际将更改传播到 initramfs 之前,我们应该始终使用这个不错的命令对其进行测试。否则,您最终必须从系统中锁定,并且只能通过chroot环境恢复它。

@andrew发布了一种使用隐藏在文件系统原始区域中的数据的好方法。但是,我发现当我们想要自动创建分区并将原始数据dd到大量usbkey时,这很烦人,我们必须计算所有不同文件系统和不同分区大小的偏移量。此外,如果用户不小心写入 FS,则存在密钥被过度使用的风险。在这种情况下,没有任何 FS 的原始分区更有意义。但是,原始分区没有UUID,这对于自动解锁不是很有用。因此,我想介绍一种仅在usbkey文件系统上使用普通密码文件的方法。passdev的主要问题是它在读取文件时不会寻求/停止。因此,当我们想要回退到键盘输入时,我们不能使用键文件偏移量和键文件大小选项。因为 cryptsetup 实际上会尝试跳过输入内容,如果内容短于密钥文件大小,它会引发错误。这也意味着如果偏移量很大,passdev 可能会非常慢,因为它总是从头开始读取。但是,没有必要为文件系统上的实际文件实现偏移量和密钥文件大小。我相信这些是为原始设备创建的。

地穴塔布

luks-part UUID="<uuid>" /dev/disk/by-uuid/<keyfile FS uuid>:/<keyfile path relative to usbkey root>:<timeout in sec> luks,keyfile-offset=<seek to the key>,keyfile-size=<>,keyscript=/bin/passphrase-from-usbfs.sh,tries=<number of times to try>,initramfs

键盘脚本 passphrase-from-usbfs.sh 利用/lib/cryptsetup/scripts/passdev,该将等待USB设备并安装FS,然后通过管道传输文件内容。它支持CRYPTTAB_KEY格式的/device-path/<keyfile FS uuid>:/<keyfile path relative to usbkey root>:<timeout in sec>

#!/bin/sh
#all message need to echo to stderr, the stdout is used for passphrase
# TODO: we may need to do something about the plymouth
echo "CRYPTTAB_KEY=$CRYPTTAB_KEY" >&2
echo "CRYPTTAB_OPTION_keyfile_offset=$CRYPTTAB_OPTION_keyfile_offset" >&2
#set your offset and file size here if your system does not support those paramters
#CRYPTTAB_OPTION_keyfile_offset=
#CRYPTTAB_OPTION_keyfile_size=
echo "timeout=$CRYPTTAB_OPTION_keyfile_timeout" >&2
CRYPTTAB_OPTION_keyfile_timeout=10 # keyfile-timeout is not supported yet 
pass=$(/lib/cryptsetup/scripts/passdev $CRYPTTAB_KEY)
rc=$?
if ! [ $rc -eq 0 ]; then
    echo "Can't find $CRYPTTAB_KEY; USB stick not present?" >&2
    /lib/cryptsetup/askpass "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME) Enter passphrase: "
else
    echo "successfully load passphrase." >&2
    echo -n $pass
fi

钩子告诉更新初始化复制我们的脚本。

#!/bin/sh
PREREQ=""
prereqs() {
        echo "$PREREQ"
}
case "$1" in
        prereqs)
                prereqs
                exit 0
        ;;
esac
. "${CONFDIR}/initramfs.conf"
. /usr/share/initramfs-tools/hook-functions
copy_exec /bin/passphrase-from-usbfs.sh
copy_exec /bin/passphrase-from-usb.sh
#when using passdev we need to hook additionaly FS and binary
copy_exec /lib/cryptsetup/scripts/passdev
manual_add_modules ext4 ext3 ext2 vfat btrfs reiserfs xfs jfs ntfs iso9660 udf

最后,我发布了 passphrase-from-usb.sh 的更新版本,它可以在crypttab中使用新参数:

为了配合上面的优秀答案,请参阅可用于编写/生成和读取原始块设备密钥的 C 例程。"readkey.c"从块设备中提取给定大小的密钥,"writekey.c"可以生成现有密钥或将现有密钥写入原始设备。编译后的"readkey.c"可以在自定义脚本中使用,从原始块设备中提取已知大小的密钥,如下所示:

readkey </path/to/device> <keysize>

要查看"writekey"的用法,编译后运行它,没有标志。
要编译,只需使用:

gcc readkey.c -o readkey
gcc writekey.c -o writekey

我在 Verbatim 16GB USB 2.0 USB 闪存驱动器上测试了两者,并在下面发布的 crypttab 中使用自定义"keyscript="。"crypto-usb.sh"的想法来自"debian etch"密码设置指南。

crypto-usb.sh

#!/bin/sh
echo ">>> Trying to get the key from agreed space <<<" >&2
modprobe usb-storage >/dev/null 2>&1
sleep 4
OPENED=0
disk="/sys/block/sdb"
boot_dir="/boot"
readkey="/boot/key/readkey"
echo ">>> Trying device: $disk <<<" >&2
F=$disk/dev
if [ 0`cat $disk/removable` -eq 1 -a -f $F ]; then
    mkdir -p $boot_dir
    mount /dev/sda1 $boot_dir -t ext2 >&2
    echo ">>> Attempting key extraction <<<" >&2
    if [ -f $readkey ]; then
        # prints key array to the caller
        $readkey /dev/sdb 4096
        OPENED=1
    fi
    umount $boot_dir >&2
fi

if [ $OPENED -eq 0 ]; then
    echo "!!! FAILED to find suitable key !!!" >&2
    echo -n ">>> Try to enter your password: " >&2
    read -s -r A
    echo -n "$A"
else
    echo ">>> Success loading key <<<" >&2
fi

当生成密钥大小时,必须提供密钥,生成的密钥将保存到具有文件权限 0600 的".tmpckey"文件中供以后使用。写入现有密钥时,大小是通过测量现有密钥大小来确定的。这看起来像是复杂的方法,但是一旦使用简单的"gcc"编译,它就可以提供操作原始密钥内容的简单方法。

readkey.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(int argc, char *argv[])
{
    int blockSize = 512;
    int keySize = 2048; 
    FILE *device;       
    if (  argc == 3 
           && (sizeof(argv[1]) / sizeof(char)) > 1
           && (sizeof(argv[2]) / sizeof(char)) > 1
       && (atoi(argv[2]) % 512) == 0
       ) {
        device = fopen(argv[1], "r");
        if(device == NULL) { 
            printf("nI got trouble opening the device %sn", argv[1]);
            exit(EXIT_FAILURE);
        }
        keySize = atoi(argv[2]);        
    }
    else if (  argc == 2 
            && (sizeof(argv[1]) / sizeof(char)) > 1
        ) {
        device = fopen(argv[1], "r");
        if(device == NULL) { 
            printf("nI got trouble opening the device %sn", argv[1]);
            exit(EXIT_FAILURE);
        }
    }
    else {
        printf("nUsage: n");
        printf("nKey Size Provided: n");
        printf("nttreadkey </path/to/device> <keysize> n");
        printf("nDefault key size: %dn", keySize);
        printf("nttreadkey </path/to/device>n");
        exit(1);
    }
    int count;
    char *block;
    /* Verify if key is multiple of blocks */
    int numBlocks = 0;
    if (keySize % 512 != 0) {
       printf("nSory but key size is not multiple of block size, try again. TA.n");
       exit(1);
    }
    /* Seek till the end to get disk size and position to start */
    fseek(device, 0, SEEK_END);
    /* Determine where is the end */
    long endOfDisk = ftell(device);
    /* Make sure we start again */
    rewind(device); // Do I need it ???
    /* Get the required amount minus block size */
    long startFrom = endOfDisk - blockSize - keySize;
    /* Allocate space for bloc */
    block = calloc(keySize, sizeof(char));
    /* Start reading from specified block */
    fseek(device, startFrom, SEEK_SET);
    fread(block, 1, keySize, device);
    /* Do something with the data */
    for(count = 0; count < keySize/*sizeof(block)*/; count++){
        printf("%c", block[count]);
    }
    /* Close file */
    fclose(device);
    /* Make sure freed array is zeroed */
    memset(block, 0, keySize);
    free(block);
}

写键.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
    int blockSize = 512;
    int keySize = 2048;
    int count;
    unsigned char *block;
    /*
        Thing to always remember that argv starts from 0 - the name of the program, and argc starts from 1 i.e. 1 is the name of the program.
    */
    if ( argc == 3 
       && strcmp(argv[1], "genwrite") != 0
       && (sizeof(argv[2]) / sizeof(char)) > 2
       ) {
        char ch;
        FILE *keyF;
        keyF = fopen(argv[1], "r");
        if (keyF == NULL) exit(EXIT_FAILURE);
        /* Tell key Size */
        fseek(keyF, 0, SEEK_END);
        keySize = ftell(keyF);
        rewind(keyF);
        printf("nKey Size: %dn", keySize);
        block = calloc(keySize, sizeof(char));
        printf("n-- Start Key --:n");
                for(count = 0; count < keySize/*sizeof(block)*/; count++){
            char ch = fgetc(keyF);
                        block[count] = ch;
            /*
              Uncomment below to see your key on screen
            */
            // printf("%c",ch);
                }
        printf("n-- End Key --:n");
        fclose(keyF);
    }
    else if (  argc == 3 
        && strcmp(argv[1], "genwrite") == 0 
        && (sizeof(argv[2]) / sizeof(char)) > 2
        ) 
        {
        printf("n-- Attempting to create random key(ish --) of size: %dn", keySize);
        block = calloc(keySize, sizeof(char));
        int count;
        for(count = 0; count < keySize/*sizeof(block)*/; count++){
            block[count] = (char) rand();
        }
        FILE *tmpfile;
        tmpfile = fopen(".tmpckey", "w");
        if(tmpfile == NULL) exit(EXIT_FAILURE);
        fwrite(block, 1, keySize, tmpfile);
        fclose(tmpfile);
        chmod(".tmpckey", 0600);
    }
    else if (  argc == 4 
        && strcmp(argv[1], "genwrite") == 0
        && (sizeof(argv[2]) / sizeof(char)) > 2
        && ((atoi(argv[3]) % 512) == 0)
        ) 
        {
        keySize = atoi(argv[3]);
        printf("n-- Attempting to create random key(ish --) of size: %dn", keySize);
        block = calloc(keySize, sizeof(char));
        int count;
        for(count = 0; count < keySize/*sizeof(block)*/; count++){
            block[count] = (char) rand();
        }
        FILE *tmpfile;
        tmpfile = fopen(".tmpckey", "w");
        if(tmpfile == NULL) exit(EXIT_FAILURE);
        fwrite(block, 1, keySize, tmpfile);
        fclose(tmpfile);
        chmod(".tmpckey", 0600);
    }   
    else {
        printf("n");
        printf("################################################################################n");
        printf("#                                                                              #n");
        printf("#                              Usage:                                          #n");
        printf("#                                                                              #n");
        printf("################################################################################n");
        printf("#> To write existing key to device:                                            #n");
        printf("#                                                                              #n");
        printf("#     writekey </path/to/keyfile> </path/to/removable/sd*>                     #n");
        printf("#                                                                              #n");
        printf("#> To generate and write pseudo random key,                                    #n");
        printf("#> key will be saved to temporary file .tmpckey                                #n");
        printf("#                                                                              #n");
        printf("#     writekey genwrite </path/to/removable/sd*> <keysize in multiples of 512> #n");
        printf("#                                                                              #n");
        printf("#> When keysize is not provided default size is set to %d.                     #n", keySize);
        printf("#                                                                              #n");
        printf("################################################################################n");
        exit(1);
    }
    /*
        Some printf debugging below, uncomment when needed to see what is going on.
    */
    /*
    printf("nNumber of Args: %dn", argc);
    printf("nCurrently block array contains: n");
    for(count = 0; count < keySize; count++){
        printf("%c", block[count]);
    }
    printf("n-- End block -- n");
    */
    /* Open Device itp... */
    FILE *device = fopen(argv[2], "a");
    if(device == NULL) exit(EXIT_FAILURE);
    printf("nDevice to write: %sn", argv[2]);
    fseek(device, 0, SEEK_END);
    /* Determine where is the end */
    long endOfDisk = ftell(device);
    printf("nDevice Size: %ldn", endOfDisk);
    /* Verify if key is multiple of blocks */
    int numBlocks = 0;
    if (keySize % 512 != 0 || endOfDisk < (blockSize + keySize) ) {
            printf("nSorry but key size is not multiple of block size or device you trying to write to is too small, try again. TA.n");
        fclose(device);
            exit(1);
    }

    /* Make sure we start again */
    rewind(device);
    /* Get the required amount sunbstracting block size */
    long startFrom = endOfDisk - blockSize - keySize;
    /* Write some data to the disk */
    printf("nWriting data starting from: %ldn", startFrom);
    fseek(device, startFrom, SEEK_SET);
    fwrite(block, 1, keySize, device);
    printf("nBlock Position after data write procedure : %ldn", ftell(device));
    /*
        Below is just for convenience, to read what was written,
        can aid in debugging hence left commented for later.
    */
    /*
    printf("nAmount of Data written : %ldn", ftell(device) - startFrom);
    // Start reading from specified block 
    printf("n>>>>>>>> DEBUGGING SECTION <<<<<<<<<n");
    rewind(device); //
    fseek(device, startFrom, SEEK_SET);
    printf("nBlock Position before read attempted: %dn", ftell(device));
    printf("nKey size: %dn", keySize);
    fread(block, 1, keySize, device);
    // Do something with the data
    printf("nBlock Position startFrom: %ldn", startFrom);
    printf("nBlock Position after read: %dn", ftell(device));
    printf("n-- Buffer Read: --n");
    for(count = 0; count < keySize; count++){
        printf("%c", block[count]);
    }
    printf("n-- End block -- n");
    printf("n--  -- n");
    printf("n--  -- n");
    */
    /* Close file */
    fclose(device);
    /* Make sure freed array is zeroed */
    memset(block, 0, keySize);
    free(block);
/* Return success, might change it to be useful return not place holder */
return 0;
}

要验证写入原始设备的密钥是否与文件中的密钥相同(如果密钥相同,则不会输出任何内容):

diff -B <(./readkey </path/to/device> 4096) <(cat .tmpckey)

或者对于使用自己的方式生成的现有密钥:

diff -B <(./readkey </path/to/device> <generated elsewhere key size>) <(cat </path/to/keyfile>)

谢谢

这是一个类似于安德鲁的解决方案,但是

  • 使用 Debian crypttab 手册页中描述的CRYPTTAB_TRIED来区分尝试,以及

  • 第一次尝试时调用现有的标准键盘脚本/lib/cryptsetup/scripts/passdev

  1. 像往常一样为 passdev 脚本创建密钥文件或密钥分区。

  2. /usr/local/bin/key-from-usb创建以下文件并使其可执行。

    #!/bin/sh
    set -e
    if [ $CRYPTTAB_TRIED -ge 1 ]; then
      /lib/cryptsetup/askpass "Second try to unlock $CRYPTTAB_SOURCE ($CRYPTTAB_NAME). Please enter passphrase: "
    else
      /lib/cryptsetup/scripts/passdev $CRYPTTAB_KEY
    fi
    
  3. /etc/crypttab中使用参数 keyscript=/usr/local/bin/key-from-usb

  4. 使用此内容创建/etc/initramfs-tools/hooks/key-from-usb

    #!/bin/sh
    PREREQ=""
    prereqs() {
            echo "$PREREQ"
    }
    case "$1" in
             prereqs)
                     prereqs
                     exit 0
             ;;
    esac
    . "${CONFDIR}/initramfs.conf"
    . /usr/share/initramfs-tools/hook-functions
    manual_add_modules vfat
    copy_exec /usr/lib/cryptsetup/scripts/passdev /usr/lib/cryptsetup/scripts/passdev
    copy_exec /usr/local/bin/key-from-usb /usr/local/bin/key-from-usb
    

    这里需要第一行copy_exec,因为如果crypttab中没有提到passdev,就不会复制它。同样,manual_add_modules vfat将确保 vfat USB 磁盘仍然可以使用。

提示:使用 lsinitramfs /boot/initrd.img-... 和 diff/compare 结果来检查脚本及其所有依赖项是否包含在内。

最新更新