基于 GPG 的密码钱包
像许多网瘾者一样,我有太多用户名/密码帐户要记住:社交网站上的帐户、工作中很少使用的登录名、网上银行等等。解决这个问题的一个方法是在所有地方使用相同的用户名和密码,但这显然不安全;如果人们在一个地方获得了您的帐户信息,他们也就拥有了您的所有其他帐户。
我想要一种相对安全、灵活且简便的方法来存储密码和其他有用的机密信息。我还希望它易于访问,这意味着我希望通过纯文本 SSH 连接来访问它。而且,我希望它能够轻松地在机器之间移动。
几个月前,我在 linux.ocm 上看到 Duane Odom 发表的一篇文章,内容是关于一个 shell 脚本,该脚本使用 GPG 加密和解密一个文本文件,其中包含用户的密码列表(或任何类型的文本)。我喜欢这种方法,因为它满足以下要求
它将密码存储在一个经过良好加密的文本文件中(受主密码保护)。该文本文件可以包含任何内容,并且可以以我想要的任何方式进行格式化。
整个界面都是文本(一个 ncurses 密码界面,后跟less或像 vim 这样的文本编辑器),因此您可以通过非图形 SSH 会话访问它(请参阅“从朋友家的计算机访问您的密码钱包”侧边栏)。
该脚本构建在大多数 Linux 发行版通用的标准实用程序之上(gpg和dialog).
从朋友家的计算机访问您的密码钱包
钱包对我来说有用的原因之一是能够从任何地方访问它。以下是一些关于启用 SSH 访问您家中宽带连接的 Linux 盒子的提示。
与其尝试记住您计算机的 IP 地址(无论如何它可能会意外更改),不如注册一个免费的动态 DNS 服务,例如 DynDNS。这使您可以为您的计算机选择一个易于记忆的主机名,例如 carlslinuxbox.dyndns.org。其中一些服务希望您定期更新您的 DNS 记录(我认为 dyndns.org 希望至少每月更新一次)。与其手动执行此操作,不如在 cron 中运行一个自动更新程序(如 inadyn)。设置更新频率时要小心——如果更新过于频繁,某些动态 DNS 服务会暂停您的帐户(阅读细则)。
如果您要让 Internet 与您 Linux 盒子上的 SSH 通信,您可以做一些事情使其更安全。我建议禁用 sshd_config 文件中的 PermitRootLogin 选项。您可能还希望使用 sshd_config 中的 Port 选项在非标准端口上运行 SSH。如果脚本小子发现 SSH 在端口 22 上运行,他们会向它抛出一堆用户名和密码,试图闯入。在 22 以外的端口上运行 SSH 在很大程度上阻止了此类事情。并且,确保您的防火墙允许访问您使用的任何端口。最后,如果您的计算机位于消费级电缆/DSL 路由器后面,您必须配置路由器以将 SSH 流量转发到您的 Linux 盒子。
完成这些操作后,下次您在朋友家时,您可以跳到一台计算机上,下载一个 SSH 客户端(例如 putty)并通过 SSH 连接到您的 Linux 盒子(记住告诉 SSH 客户端您的动态 DNS 主机名以及您运行 SSH 的端口号)。
虽然我喜欢原始脚本的工作方式,但我希望添加几个功能。所以我对原始脚本进行了一些修改,结果如清单 1 所示。
清单 1. wallet 脚本
1 #!/bin/bash 2 3 . ~/bin/functions 4 is_installed gpg 5 is_installed dialog 6 is_installed mktemp 7 is_installed basename 8 9 if [ -f ~/.walletrc ]; then 10 . ~/.walletrc 11 fi 12 13 if [ -z $VISUAL ]; then 14 VISUAL=vi 15 fi 16 17 EDIT_PWFILE=0 18 while getopts 'ec:' OPTION 19 do 20 case $OPTION in 21 e) EDIT_PWFILE=1;; 22 c) WALLET_FILENAME="$OPTARG";; 23 ?) printf "usage: %s [ -e ] [ -c encrypted_file ]\n" \ 24 $( basename $0 ) >&2 25 exit 1 26 ;; 27 esac 28 done 29 shift $(($OPTIND - 1)) 30 31 if [ -z "$WALLET_FILENAME" ]; then 32 echo "need the encrypted file specified by WALLET_FILENAME" 33 echo "(in ~/.walletrc or the envariable) or with the -c option" 34 exit 2 35 fi 36 37 if [ ! -f $WALLET_FILENAME ]; then 38 echo "$WALLET_FILENAME doesn't exist--attempting to create..." 39 echo "(you'll need to give gpg a master password)" 40 mkdir -p $( dirname $WALLET_FILENAME ) 41 TEMPFILE=$( mktemp /tmp/wallet.XXXXXX ) 42 gpg -c -o $WALLET_FILENAME $TEMPFILE 43 rm -f $TEMPFILE 44 EDIT_PWFILE=1 45 fi 46 47 if [ $EDIT_PWFILE -eq 1 ]; then 48 is_installed $VISUAL 49 fi 50 51 # prompt the user for the password 52 PASSWORD=$( dialog --stdout --backtitle "Password Wallet" \ 53 --title "Master Password" --clear --passwordbox \ 54 "Please provide the master password." 8 40 ) 55 if [ $? -ne 0 ]; then 56 echo "Failed to acquire master password" 57 exit 4 58 fi 59 if [ -z $PASSWORD ]; then 60 echo "Password is required" 61 exit 8 62 fi 63 64 # if we're not editing the file, just display it and quit 65 if [ $EDIT_PWFILE -eq 0 ]; then 66 echo $PASSWORD | gpg --decrypt --passphrase-fd 0 \ 67 $WALLET_FILENAME | less 68 clear 69 exit 0 70 fi 71 72 # set up the directory in which the unencrypted wallet file 73 # will be edited 74 TMPDIR=$( mktemp -d /tmp/wallet.XXXXXX ) 75 CLEARTEXT_WALLET_FILENAME=$TMPDIR/wallet 76 77 # try to ensure that cleartext wallet file is deleted, 78 # even after unexpected terminations 79 trap "{ rm -rf $TMPDIR; }" 0 1 2 5 15 80 81 # decrypt the password wallet--an error here probably means 82 # the user typed the wrong password to decrypt the wallet 83 echo $PASSWORD | gpg -o $CLEARTEXT_WALLET_FILENAME \ 84 --passphrase-fd 0 \ 85 $WALLET_FILENAME &> /dev/null 86 case $? in 87 0) 88 # decryption succeeded, so open the wallet in the editor 89 # and then re-encrypt it when the editor closes 90 mv $WALLET_FILENAME ${WALLET_FILENAME}.bak 91 $VISUAL $CLEARTEXT_WALLET_FILENAME 2> /dev/null 92 echo $PASSWORD | gpg -c -o $WALLET_FILENAME \ 93 --passphrase-fd 0 \ 94 $CLEARTEXT_WALLET_FILENAME &> /dev/null 95 if [ $? -eq 0 ]; then 96 clear 97 else 98 LAST_RESORT_FILENAME=$( mktemp ~/wallet.XXXXXX ) 99 cp $CLEARTEXT_WALLET_FILENAME $LAST_RESORT_FILENAME 100 chmod 600 $LAST_RESORT_FILENAME 101 echo "gpg failed to enrypt your password wallet: I have" 102 echo "tried to put a CLEARTEXT copy of your wallet at" 103 echo $LAST_RESORT_FILENAME 104 exit 16 105 fi 106 exit 0;; 107 ?) 108 echo "error condition detected (invalid password?)" 109 exit 32;; 110 esac
它非常容易安装;只需将脚本保存到 $PATH 中的某个位置并使其可执行即可。然后,您只需要告诉它您的加密密码文件应该在哪里。有三种方法可以做到这一点
设置 $WALLET_FILENAME 环境变量。
在 ~/.walletrc 中设置 $WALLET_FILENAME。
使用 -c 命令行选项指定文件名。
第二种方法(覆盖第一种方法)是我的首选——我在 ~/.walletrc 中有以下行
WALLET_FILENAME=~/docs/wallet.gpg
但是,如果我需要使用不同的钱包文件,我可以使用命令行选项通过像这样调用脚本来覆盖前两种方法中的任何一种
wallet -c ~/docs/other_wallet.gpg
wallet 默认为只读模式,在这种模式下,它使用 less 显示您的钱包文件的解密版本。但是,如果您包含 -e 命令行选项(编辑模式),脚本会将您的钱包文件解密到临时位置并在文本编辑器中打开它(脚本默认为使用 vi,但您可以在环境或 ~/.walletrc 文件中设置 $VISUAL 变量)。当您关闭编辑器时,wallet 会加密该文件并将其保存到原始位置。
您第一次运行 wallet 时,您将没有钱包文件,因此 wallet 会为您创建它并在编辑模式下运行。
让我们深入研究脚本,看看它是如何工作的。它做的第一件事是使用点运算符来源文件名为 functions 的文件,该文件如清单 2 所示。让 wallet 源一个外部文件(使用点运算符)本质上等同于在 wallet 的第 3 行插入源文件的内容(~/bin/functions)。这样做允许其他脚本使用相同的代码(shell 脚本的代码库)。
清单 2. functions 文件
is_installed() { PROGRAM=$1 PATHNAME=$( type $PROGRAM 2> /dev/null ) if [ -z "$PATHNAME" ]; then echo "cannot locate $PROGRAM in path" exit 1 fi }
functions 文件包含一个名为 is_installed 的函数,该函数使用 bash 内置类型来查看是否安装了程序。如果 is_installed 在您的 $PATH 中找不到该程序,则 is_installed 会打印一条错误消息并调用 exit,这将终止 wallet。因此,如果您运行 wallet 并且它因类似“cannot locate dialog in path”的错误而退出,则您可能尚未安装 dialog 包。使用您的发行版的包管理系统(yum、apt-get 等)安装 dialog 并重试。
wallet 脚本的第 18 行到第 28 行使用 getopts bash 内置函数解析命令行参数。while 循环遍历字符串 ec: 指定的选项。这意味着 wallet 可以接受 -e 和 -c 选项,并且 -c 选项需要一个参数。当 while 循环遍历命令行参数时,当前选项被分配给变量 $OPTION,当前选项的任何参数都被分配给变量 $OPTARG。任何无法识别的选项都会导致错误消息,并且 wallet 退出。while 循环完成后,重置 $OPTIND 变量非常重要(这在任何 getopts 调用后都是必要的)。
wallet 脚本的第 37 行到第 45 行验证加密文件是否存在,如果尚不存在则创建该文件。-f 测试检查 $WALLET_FILENAME 是否作为普通文件存在。如果不存在,则测试失败,并且 wallet 假定您是第一次运行 wallet,并且 wallet 需要设置工作环境。wallet 使用命令替换语法来创建加密文件应该存在的目录(第 40 行)
mkdir -p $( dirname $WALLET_FILENAME )
$(...) 中的命令首先运行,结果成为 mkdir 的参数。dirname 命令返回加密文件的目录,mkdir -p 创建该目录(以及任何必要的父目录)。
接下来,wallet 需要创建加密文件(即使未加密版本将为空)。第 41 行使用 mktemp 在 /tmp 中创建一个空文件,其名称以六个随机选择的字符结尾。mktemp 打印它创建的文件的名称,因此在命令替换 shell 中运行此命令并将结果分配给 $TEMPFILE 会将临时文件的名称放入 $TEMPFILE。
现在我们看到了 gpg 的第一次使用。第 42 行使用 gpg 通过对称加密(gpg 的 -c 选项)加密(空)临时文件 ($TEMPFILE) 并将加密文件写入 $WALLET_FILENAME。然后 wallet 删除临时文件。因为这是 wallet 第一次运行,所以它假定编辑模式是合适的并设置 $EDIT_PWFILE 标志。
第 52 行再次使用命令替换技巧,这次是提示用户输入主密码(用于加密钱包文件)。dialog 手册页描述了使用 dialog 的脚本可以从用户处检索输入的多种方式。此示例使用 dialog 创建一个简单的密码框。--stdout 选项告诉 dialog 将用户的输入(主密码)打印到标准输出,以便可以将其分配给 $PASSWORD。
第 55 行检查 bash 变量 $?,其中包含上一个进程(在本例中为 dialog)的退出代码。约定是退出代码 0 表示成功(并且 wallet 在其自己的退出调用中遵循此约定)。如果第 55 行的 $? 与 0 不同,则表示 dialog 遇到错误,并且 wallet 终止并显示错误消息。
如果 $EDIT_PWFILE 为 0(第 65 行),则 wallet 以只读模式运行
echo $PASSWORD | gpg --decrypt --passphrase-fd 0 ↪$WALLET_FILENAME | less
这告诉 gpg 解密 $WALLET_FILENAME 并从标准输入 (fd 0) 读取密码。将 $PASSWORD 管道传输到 gpg 使 gpg 能够在不交互式询问用户主密码的情况下解密钱包文件。输出(解密的钱包文件)被打印到标准输出,标准输出被管道传输到 less,允许用户翻阅密码、运行搜索等等。当用户关闭 less 时,wallet 清除屏幕并退出。
脚本的其余部分假定 $EDIT_PWFILE 为非零值(即 wallet 以编辑模式运行)。
在编辑模式下,wallet 需要解密钱包文件,在文本编辑器中打开解密的文件,然后将编辑后的文件加密回原始位置。第 74 行使用 mktemp 创建一个临时目录,钱包文件将在其中解密。第 75 行将 $CLEARTEXT_WALLET_FILENAME 设置为临时目录中文件的名称。
第 79 行运行 trap,这是一个 bash 内置函数。trap 的第一个参数是一个命令,后面跟一个信号列表(例如,如果有人运行kill在 wallet 上)。如果 wallet 在第 79 行之后收到任何这些信号,wallet 将在退出之前运行捕获的命令(删除解密的钱包文件)。这是一种尝试,以确保如果 wallet 意外终止,解密的文件不会被遗留在周围。
第 83 行类似于我们在只读模式下看到的,但 gpg 添加了 -o 选项。这指示 gpg 将解密的文件写入 $CLEARTEXT_WALLET_FILENAME。
如果 gpg 的退出代码为 0,则 wallet 会将加密的钱包文件重命名为带有 .bak 扩展名(从而保留副本,以防出现问题)并在文本编辑器 $VISUAL 中打开解密的文件。编辑器退出后,wallet 告诉 gpg 加密 $CLEARTEXT_WALLET_FILENAME 处的编辑后的纯文本文件,并将加密的钱包文件写回 $WALLET_FILENAME。来自此 gpg 调用的非零退出状态意味着重新加密钱包文件时出现问题,因此 wallet 会在您的主目录中复制纯文本文件并打印错误消息。
清单 3. 密码生成器脚本
#!/bin/bash . ~/bin/functions is_installed openssl DIGEST="sha1" RULER=0 DASH_N="" while getopts 'mrn' OPTION do case $OPTION in m) DIGEST="md5";; r) RULER=1;; n) DASH_N="-n";; ?) printf "usage: %s [ -m ] [ -r ]\n" $( basename $0 ) >&2 exit 2 ;; esac done shift $(($OPTIND - 1)) if [ ! -z $DASH_N ]; then RULER=0 fi DD=$( dd if=/dev/urandom bs=1k count=1 2> /dev/null \ | openssl dgst -$DIGEST ) echo $DASH_N $DD if [ $RULER -eq 1 ]; then echo ' 5| 10| 15| 20| 25| 30| 35| 40|' fi
密码生成器
清单 3 显示了一个简短的 shell 脚本,该脚本生成非常随机、难以记住的密码——非常适合存储在您的钱包中。mkpass 将一千字节的随机数据转储到摘要算法中以生成 ASCII 密码。默认情况下,mkpass 使用 SHA1 摘要算法,但如果您提供 mkpass 的 -m 命令行选项,则可以使用 MD5。并且,如果您提供 -r 选项,mkpass 会在密码下方打印标尺(如果您需要或想要特定长度的密码,则很有用)。
如果您是 vim 用户,请尝试将以下行添加到您的 ~/.vimrc 文件
map \mkpass i <CR><ESC>k$:r!~/bin/mkpass -n<CR>kJJ
现在当您运行 vim 时(就像您在编辑模式下使用 wallet 时一样),键入\mkpass在命令模式下,将在光标位置插入密码。
资源
Duane Odom 的“如何创建命令行密码库”:www.linux.com/feature/114238
DynDNS 动态 DNS:www.dyndns.org
inadyn 动态 DNS 更新程序:inadyn.ina-tech.net
putty SSH 客户端:www.chiark.greenend.org.uk/~sgtatham/putty
Carl Welch 是一位 Web 开发人员和 Linux 系统管理员。他喜欢科幻小说,对牙医感到矛盾,并且不喜欢标准灯开关。他在 mbrisby.blogspot.com 上维护着地球上最糟糕的博客。