我想建立一个具有全磁盘加密功能的无头Linux(Debian Wheezy)PC,能够通过USB驱动器解锁磁盘,或者通过键盘输入密码。我的出发点是在Debian安装程序中使用基本的整个磁盘加密选项进行全新安装,除了/ boot作为LUKS加密的逻辑卷组之外,它还管理键盘选项。我将在答案中描述我当前的解决方案,希望它有用,其他人可以改进它。
以下是我遇到的一些问题:
设置密码并将其放在USB驱动器上。
及时加载USB模块。
在尝试读取之前,等待USB驱动器被Linux识别。
识别正确的USB驱动器(不是恰好插入的其他驱动器)。
写一个“keyscript”来从USB驱动器中拔出密码。
确保在所有USB故障情况下,键盘的后退都会出现。
我会接受一个有重大改进的答案,并提供有助于提供贡献的答案。
答案 0 :(得分:20)
我的很多解决方案都来自帖子Using A USB Key For The LUKS Passphrase。
创建随机密码:
dd if=/dev/urandom bs=1 count=256 > passphrase
插入USB驱动器。 dmesg
输出将显示设备名称;假设/dev/sdd
。弄清楚它的大小:
blockdev --getsize64 /dev/sdd
我决定在原始设备的末尾安装密码短语,以确定它可能会在任何意外使用USB驱动器后继续存在。
dd if=passphrase of=/dev/sdd bs=1 seek=<size-256>
将密码短语添加到LUKS卷:
cryptsetup luksAddKey /dev/sda5 passphrase
这不会影响安装程序现有的手动输入密码。密码短语文件可以删除:
rm passphrase
找到USB记忆棒的唯一名称,以便我们在出现时识别它:
ls -l /dev/disk/by-id | grep -w sdd
你应该看到一个符号链接。我会称之为/dev/disk/by-id/<ID>
。
修改/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
上面提到的keyscript
需要从USB设备读取密码。但是,它还需要做更多的事情。要了解它的使用方法,请检查/usr/share/initramfs-tools/scripts/local-top/cryptroot
,即启动时运行的脚本以解锁根设备。请注意,当设置keyscript
时,只需运行它,输出就会传送到luksOpen
而不进行其他检查。无法发出错误信号(USB驱动器不存在)或回退到键盘输入。如果密码短语失败,则会在循环中再次运行keyscript,最多会运行一次;但是我们没有被告知我们正在进行哪次迭代。此外,我们无法控制keyscript何时运行,因此我们无法确定Linux是否识别了USB驱动器。
我用一些黑客解决了这个问题:
轮询USB驱动器并等待3秒钟以显示它。这对我有用,但我很想知道更好的方法。
首次运行时创建一个虚拟文件/passphrase-from-usb-tried
,表示我们至少运行过一次。
如果我们至少运行过一次,或者找不到USB驱动器,请运行askpass
用于键盘输入的cryptroot
程序。
最终剧本:
#!/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
我的initramfs中没有USB驱动程序。 (默认情况下,它们在Debian的更高版本中显示。)我必须通过添加/etc/initramfs-tools/modules
来添加它们:
uhci_hcd
ehci_hcd
usb_storage
完成所有操作后,请更新initramfs:
update-initramfs -u
答案 1 :(得分:4)
如果我可以简单地使用一个包含密码的小型USB记忆棒,那将是理想的选择 解锁磁盘。这不仅对服务器很方便(你可以把USB棒放在那里) 服务器 - 目标是能够返回破损的硬盘而不必担心机密数据),这对我的笔记本电脑来说也很棒:在启动时插入USB记忆棒并在之后将其删除 解锁密码盘。
我现在已经编写了一个补丁,它将在所有设备的根目录中搜索文件&#39; cryptkey.txt&#39;并尝试解密 以每一行为关键。如果失败:返回键入密码短语。
它确实意味着密钥不能包含\ n,但这也适用于任何键入的密钥。好的部分是您可以使用相同的USB磁盘存储多台计算机的密钥:您不需要为每台计算机单独安装一个USB磁盘。因此,如果物理钥匙环中有USB驱动器,则可以在物理关闭时为所有启动的计算机使用相同的驱动器。
您可以使用以下内容添加密钥:
cryptsetup luksAddKey /dev/sda5
然后将相同的密钥作为一行放在名为&#39; cryptkey.txt&#39;的USB / MMC磁盘上的文件中。补丁在这里:
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
找到补丁和文件答案 2 :(得分:1)
为了配合上面的优秀答案,请参阅可用于编写/生成和读取原始块设备密钥的C例程。 &#34; readkey.c&#34;从块设备中提取给定大小的密钥和&#34; writekey.c&#34;可以生成或写入原始设备的现有密钥。 &#34; readkey.c&#34;一旦编译就可以在自定义脚本中使用,从原始块设备中提取已知大小的密钥,如下所示:
readkey </path/to/device> <keysize>
要查看&#34; writekey&#34;的用法,在编译后运行它没有标志 要编译只需使用:
gcc readkey.c -o readkey
gcc writekey.c -o writekey
我在Verbatim 16GB USB 2.0 USB闪存盘上测试了自定义&#34; keyscript =&#34;在crypttab中也发表在下面。 &#34; crypto-usb.sh&#34;的想法来自&#34; debian etch&#34; cryptsetup指南。
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
当必须提供生成密钥的密钥大小时,生成的密钥将保存到&#34; .tmpckey&#34;具有文件权限0600的文件供以后使用。在写入现有密钥时,通过测量现有密钥大小来确定大小。这看起来像复杂的方法,但一旦编译简单&#34; gcc&#34;它可以提供操作原始密钥内容的简便方法。
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 %s\n", 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 %s\n", argv[1]);
exit(EXIT_FAILURE);
}
}
else {
printf("\nUsage: \n");
printf("\nKey Size Provided: \n");
printf("\n\t\treadkey </path/to/device> <keysize> \n");
printf("\nDefault key size: %d\n", keySize);
printf("\n\t\treadkey </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);
}
<强> writekey.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: %d\n", 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: %d\n", 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: %d\n", 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: %d\n", 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: %s\n", argv[2]);
fseek(device, 0, SEEK_END);
/* Determine where is the end */
long endOfDisk = ftell(device);
printf("\nDevice Size: %ld\n", 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: %ld\n", startFrom);
fseek(device, startFrom, SEEK_SET);
fwrite(block, 1, keySize, device);
printf("\nBlock Position after data write procedure : %ld\n", 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 : %ld\n", 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: %d\n", ftell(device));
printf("\nKey size: %d\n", keySize);
fread(block, 1, keySize, device);
// Do something with the data
printf("\nBlock Position startFrom: %ld\n", startFrom);
printf("\nBlock Position after read: %d\n", 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>)
谢谢
答案 3 :(得分:1)
尽管@Andrew给出了很好的答案,该答案在以前的版本中仍然有效。该解决方案实际上已经过时了,需要针对Ubuntu 18.04和19.10进行大量调整。因此,我想分享我对此的研究。
有关crypttab的一些注意事项。 Sepcs实际上从14.04到18.04以及19.10发生了很大变化。它开始支持更多的cryptsetup参数。例如keyfile-offset,keyfile-size等。某些选项例如nobootwait不见了。某些参数已在其他发行版中受支持,但在ubuntu中尚不支持(例如,非常好的参数keyfile-timeout。这可以消除整个键盘脚本,因为它将在keyfile-timeout之后自动回退到键盘输入。)
ubuntu上crypttab的主要缺陷是它实际上由2个不同的进程处理。一个是传统的initramfs,另一个是现代systemd。 Systemd在许多方面都应该更加先进和灵活。但是, systemd对crypptab的支持很差,有很多选项,例如关键字脚本只是被默默地忽略了。,所以我不知道发生了什么,直到发现this post。几乎所有有关crypttab设置的在线帖子都是针对initramfs而不是针对systemd。因此,我们需要将initramfs
添加到crypttab中的所有条目中,以避免出现问题。
我还发现了一种无需VM或反复重启即可调试我们的脚本和crypttab的好方法。是cryptdisks_start
。在我们将更改实际传播到initramfs之前,我们应该始终使用此命令进行测试。否则,您必须最终将其锁定在系统之外,并且只能通过chroot环境进行恢复。
@andrew发布了一种在文件系统原始区域中使用数据隐藏的好方法。但是,我发现当我们想要自动创建分区并将原始数据添加到大量usbkey时非常烦人,我们必须计算所有不同文件系统和不同分区大小的偏移量。此外,如果用户不小心写入FS,则存在密钥被覆盖的风险。在这种情况下,没有任何FS的原始分区更有意义。但是原始分区没有UUID,这对于自动解锁不是很有用。因此,我想介绍一种只在usbkey文件系统上使用普通密码文件的方法。 passdev的主要问题是在读取文件期间不会查找/停止。因此,当我们要回退到键盘输入时,就不能使用keyfile-offset和keyfile-size选项。因为cryptsetup实际上会尝试跳过输入内容,并且如果内容短于keyfile-size,它将引发一个错误。这也意味着,如果偏移量很大,passdev可能会非常慢,因为它总是从头读取。但是,没有必要为文件系统上的实际文件实现偏移量和密钥文件大小。我相信这些都是为原始设备创建的。
crypttab
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
该钩子告诉update-initramfs复制我们的脚本。
#!/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中的新参数:
答案 4 :(得分:1)
这里有一个类似于 one by Andrew 的解决方案,但是
使用 Debian crypttab man page 中描述的 CRYPTTAB_TRIED 来区分尝试,以及
在第一次尝试时调用现有的标准键脚本 /lib/cryptsetup/scripts/passdev
。
照常为 passdev
脚本创建您的密钥文件或密钥分区。
创建以下文件 /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
在 /etc/crypttab
中使用参数 keyscript=/usr/local/bin/key-from-usb
。
使用此内容创建 /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
行,因为如果 passdev
中未提及,则不会复制 crypttab
。同样,manual_add_modules vfat
将确保仍然可以使用 vfat usb 磁盘。
提示:使用 lsinitramfs /boot/initrd.img-...
和 diff/compare 结果来检查是否包含脚本及其所有依赖项。