基于 GPG 的密码钱包

作者:Carl Welch

像许多网瘾者一样,我有太多用户名/密码帐户要记住:社交网站上的帐户、工作中很少使用的登录名、网上银行等等。解决这个问题的一个方法是在所有地方使用相同的用户名和密码,但这显然不安全;如果人们在一个地方获得了您的帐户信息,他们也就拥有了您的所有其他帐户。

我想要一种相对安全、灵活且简便的方法来存储密码和其他有用的机密信息。我还希望它易于访问,这意味着我希望通过纯文本 SSH 连接来访问它。而且,我希望它能够轻松地在机器之间移动。

几个月前,我在 linux.ocm 上看到 Duane Odom 发表的一篇文章,内容是关于一个 shell 脚本,该脚本使用 GPG 加密和解密一个文本文件,其中包含用户的密码列表(或任何类型的文本)。我喜欢这种方法,因为它满足以下要求

  1. 它将密码存储在一个经过良好加密的文本文件中(受主密码保护)。该文本文件可以包含任何内容,并且可以以我想要的任何方式进行格式化。

  2. 整个界面都是文本(一个 ncurses 密码界面,后跟less或像 vim 这样的文本编辑器),因此您可以通过非图形 SSH 会话访问它(请参阅“从朋友家的计算机访问您的密码钱包”侧边栏)。

  3. 该脚本构建在大多数 Linux 发行版通用的标准实用程序之上(gpgdialog).

从朋友家的计算机访问您的密码钱包

钱包对我来说有用的原因之一是能够从任何地方访问它。以下是一些关于启用 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 中的某个位置并使其可执行即可。然后,您只需要告诉它您的加密密码文件应该在哪里。有三种方法可以做到这一点

  1. 设置 $WALLET_FILENAME 环境变量。

  2. 在 ~/.walletrc 中设置 $WALLET_FILENAME。

  3. 使用 -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

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在命令模式下,将在光标位置插入密码。

结论

wallet 是一个用于管理密码钱包的 bash 脚本。它的编写目的是可在纯文本界面上使用。希望这段代码描述能够帮助您在脚本技巧的工具箱中添加一两项。

资源

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 上维护着地球上最糟糕的博客。

加载 Disqus 评论