人脸登录后的 KWallet 自动解锁

人脸认证证明了用户身份,却不会产生登录密码,因此标准 pam_kwallet5 拿不到 PAM_AUTHTOK。用一个只读密钥 PAM 模块把钱包密钥交给认证栈,保留 KWallet 加密并覆盖 sudo、polkit 和两条登录路径。

被修正 本文已被《被人脸钱包密钥掉包的登录密码》修正,见文末。另有 1 篇提到本文,见文末。

在很长一段时间里,虽然我已经利用 `biopass` 实现了人脸识别登录,但是我的使用率却相当低。虽然人脸已经识别成功,桌面也进去了,但是 KWallet 却弹出解锁框提示输入密码。即便把 `pam_kwallet5.so` 往前挪一行,通常也不会解决问题。

先决条件

```text
CachyOS(Arch 系)|KDE Plasma 6.7.5|Plasma Login Manager 6.7.4
Wayland|biopass-bin 1.4.1|kwallet 6.30.0|kwallet-pam 6.7.5
gcc 16.2.1|pam 1.7.2
```

本机使用 `ksecretd` 作为 Plasma 6 的 Secret Service 守护进程(它也是 kwallet-pam 6.7.5 编译时的默认守护进程),钱包名是 `kdewallet`。磁盘没有 LUKS,`/etc/crypttab` 为空,所以不存在“开机解锁磁盘时顺便拿到钱包密码”的条件。

需要编译一个小模块,所以机器上要有 C 编译器和 PAM 开发头文件(Arch 系在 `pam` 包里)。下面的 PAM 文件路径、用户名和密钥路径都是示例,实际安装前先确认发行版的 PAM 包、登录管理器和钱包版本;不要把本机的 `/etc` 配置原样覆盖到另一台机器。

现象

人脸登录成功后,PLM 的日志里能看到 `pam_kwallet5` 走到了 session 阶段,但紧接着是:

```text
pam_kwallet5(plasmalogin:session): open_session called without kwallet5_key
```

同一会话里还有两条旁证:环境变量 `PAM_KWALLET5_LOGIN` 不存在,`/run/user/<uid>/kwallet5.socket` 也不存在。这两样是钱包解锁成功才会有的东西。

```bash
# 钱包到底开没开(true = 已解锁)
busctl --user call org.kde.kwalletd6 /modules/kwalletd6 \
  org.kde.KWallet isOpen s kdewallet

journalctl -b -u plasmalogin | grep -i pam_kwallet5
```

先把认证栈和 `pam_kwallet5` 的取值路径对上,再决定要不要改钱包密码。

PAM 的两段:auth 与 session

要理解钱包为什么不开,得先分清 PAM 里两个阶段各自发生什么。`pam_kwallet5` 的工作是分开的(下面按官方 `pam_kwallet.c` 的行为描述):

auth 阶段——拿密码,存起来:

  1. `pam_get_item(PAM_AUTHTOK)` 取当前认证口令;
  2. 为空的话它会尝试自己弹窗问一次;
  3. 还是空,就直接返回 `PAM_IGNORE`,源码里的日志原文是 `Empty or missing password, doing nothing`;
  4. 拿到了,就用 `pam_set_data("kwallet5_key", 密码)` 把它挂在 PAM handle 上。

session 阶段(配置行里的 `auto_start`)——用密码开钱包:

  1. 把上一步的 `kwallet5_key` 取出来;
  2. 拿 `~/.local/share/kwalletd/kdewallet.salt` 当盐,做 PBKDF2(SHA512,50000 轮,输出 56 字节)派生钱包密钥;
  3. 以用户身份拉起 `ksecretd --pam-login <管道> <套接字>`,把密钥写进管道,完成解锁。

关键在第一步和第二步之间的唯一桥梁就是那串密码。也就是说,钱包的密码必须等于 `PAM_AUTHTOK` 里的东西,否则派生出来的密钥打不开钱包文件。这是后面所有方案的出发点。

钱包为什么拿不到密码

`sufficient` 会把后面的模块整组跳过

`man pam.conf` 对 `sufficient` 的定义是:该模块成功时,如果前面没有 required 模块失败,PAM 立即向应用返回成功,跳过本组剩余模块。

本机原始的授权栈顺序是:

  1. `auth sufficient libbiopass_pam.so` ← 人脸成功,授权栈就地结束
  2. `auth include system-login`(`pam_unix` 密码)
  3. `-auth optional pam_kwallet5.so` ← 永远走不到

所以只要刷脸成功,`pam_kwallet5` 的 auth 钩子根本不执行,密码也就没机会被存下来给 session 阶段用。

就算把顺序调过来,人脸也交不出密码

把 `pam_kwallet5` 挪到 biopass 前面,它能执行了,但 `pam_get_item(PAM_AUTHTOK)` 仍然取不到东西。原因在 biopass 的 PAM 模块(按符号表核对):它只调用 `pam_get_item` 和 `pam_get_user`,然后 `fork` 加 `execl("/usr/bin/biopass-helper", "--service", <服务名>, "--username", <用户>, "auth")`,从头到尾没有调用过 `pam_set_item`。

人脸识别不产生任何可交给钱包的字符串。所以我们可以得出结论:

人脸或指纹登录 + 标准 `pam_kwallet5` = 钱包无法自动解锁。 除非密码从别处来——用户手输,或者我们自己注入。

官方与社区结论一致。Arch Wiki 的 KDE Wallet 条目明确列出这条限制(用指纹读卡器登录时钱包无法解锁),并说明自动解锁的前提是钱包名 `kdewallet`、blowfish 加密、且钱包密码等于用户登录密码。KDE bug 468069(CONFIRMED)还表明,即使在“钱包密码为空 + 没有 PAM 凭据”的情况下,KDE 也不保证自动打开钱包。

两条错误路径

只移动 `pam_kwallet5`

这只能解决 `sufficient` 提前结束的问题,不能创造 `PAM_AUTHTOK`。如果日志从“模块没执行”变成“没有密码”,根因仍然是同一个。

借用 LUKS 的开机密码

有些方案让磁盘解密密码和钱包密码相同,再由 `pam_systemd_loadkey` 转交给钱包。本机 `/etc/crypttab` 为空,磁盘没有 LUKS,这条路径没有可借用的密钥。

方案选择

方案 结果 代价
清空钱包密码 不再需要输入密码 钱包文件等同未加密
保持现状 登录后手动解锁 每次都要输一次密码
自建 PAM 模块 注入独立密钥,保留加密 多维护一个模块,密钥须由 root 保护

本机采用第三种,理由是希望保留钱包文件的静态加密:非 root 的攻击者即使拿到 `kdewallet.kwl`,没有 `/etc/pam_walletkey.key` 也解不开。而“清空密码”那条路等于把钱包变成明文文件,任何人读到你的家目录或备份都能拿到里面的 WiFi 密码、浏览器登录态和令牌。

第三种方案的三条设计边界:

  1. 密钥和登录密码不是同一个值。 钱包改用一个随机长串,登录密码照旧。
  2. 密钥只存在于 root 可读的文件里。 普通用户读不到,模块本身也不打印它。
  3. 模块读不到密钥时返回 `PAM_IGNORE`,钱包回到手动解锁。 不能因为钱包配置错误把用户锁在登录界面外。

自建模块做什么

模块只做一件事:把一个受保护文件的第一行写进 `PAM_AUTHTOK`。它要满足的校验条件:

任何一条不满足,就记一条警告日志并返回 `PAM_IGNORE`,绝不阻断登录。PAM 行上的 `optional` 控制位是第二层保险。

```c
/*
 * pam_walletkey.so — 把 root 只读密钥文件中的 KWallet 密码注入 PAM_AUTHTOK
 *
 * 用途:biopass(人脸/指纹)登录不会产生密码,导致 pam_kwallet5 拿不到
 *      PAM_AUTHTOK,KWallet 无法自动解锁。本模块在 auth 阶段把预先约定的
 *      钱包密码写入 PAM_AUTHTOK,紧随其后的 pam_kwallet5 即可正常存下并
 *      在 session 阶段解锁 kdewallet。
 *
 * 安装位置:/usr/lib/security/pam_walletkey.so
 * 密钥文件:/etc/pam_walletkey.key (root:root 0600,单行,仅含钱包密码)
 * PAM 用法:-auth optional pam_walletkey.so [keyfile=/path]
 *
 * 失败时一律返回 PAM_IGNORE,绝不阻断登录(配合 optional 控制位双保险)。
 *
 * SPDX-License-Identifier: MIT
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <pwd.h>

#include <security/pam_modules.h>
#include <security/_pam_types.h>

#define PAM_WALLETKEY_DEFAULT_FILE "/etc/pam_walletkey.key"
#define PAM_WALLETKEY_MAX          1024

static void wipe(char *s)
{
    if (!s)
        return;
    volatile char *p = s;
    while (*p)
        *(p++) = 0;
}

/* 读取并校验密钥文件,成功返回 0 */
static int read_key(const char *path, char *buf, size_t bufsz)
{
    struct stat st;

    if (stat(path, &st) != 0 || !S_ISREG(st.st_mode))
        return -1;
    /* 必须是 root 所有,且不允许 group/other 有任何权限 */
    if (st.st_uid != 0)
        return -2;
    if ((st.st_mode & (S_IRWXG | S_IRWXO)) != 0)
        return -3;

    FILE *f = fopen(path, "r");
    if (!f)
        return -4;

    if (fgets(buf, (int)bufsz, f) == NULL) {
        fclose(f);
        return -5;
    }
    fclose(f);

    size_t n = strcspn(buf, "\r\n");
    buf[n] = '\0';

    return n == 0 ? -6 : 0;
}

PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    (void)flags;

    const char *keyfile = PAM_WALLETKEY_DEFAULT_FILE;

    for (int i = 0; i < argc; i++) {
        if (strncmp(argv[i], "keyfile=", 8) == 0 && argv[i][8] != '\0')
            keyfile = argv[i] + 8;
    }

    const char *user = NULL;
    if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || user == NULL || *user == '\0')
        return PAM_IGNORE;

    /* root 不需要钱包解锁,直接跳过 */
    struct passwd *pw = getpwnam(user);
    if (pw != NULL && pw->pw_uid == 0)
        return PAM_IGNORE;

    char key[PAM_WALLETKEY_MAX] = {0};
    int rc = read_key(keyfile, key, sizeof(key));
    if (rc != 0) {
        syslog(LOG_WARNING,
               "pam_walletkey: cannot use key file %s (rc=%d); wallet unlock left to user",
               keyfile, rc);
        return PAM_IGNORE;
    }

    int r = pam_set_item(pamh, PAM_AUTHTOK, key);
    wipe(key);

    if (r != PAM_SUCCESS) {
        syslog(LOG_WARNING, "pam_walletkey: pam_set_item(PAM_AUTHTOK) failed (rc=%d)", r);
        return PAM_IGNORE;
    }

    syslog(LOG_DEBUG, "pam_walletkey: wallet key injected for user %s", user);
    return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    (void)pamh;
    (void)flags;
    (void)argc;
    (void)argv;
    return PAM_SUCCESS;
}
```

几处实现细节:

`pam_sm_setcred` 是个空实现,直接返回 `PAM_SUCCESS`。PAM 加载模块时会找这个符号,不提供会报错,但本方案在 setcred 阶段确实无事可做。

安装

分三步:编译模块、生成密钥、跑安装脚本。

1. 编译模块

把上面的 `pam_walletkey.c` 存到某个目录,然后:

```bash
gcc -shared -fPIC -O2 -o pam_walletkey.so pam_walletkey.c
```

不需要 `-lpam`。本机实测编译出的 `.so` 里未定义的符号只有 `pam_get_user` 和 `pam_set_item` 两个 PAM 符号(其余都是 libc),它们由宿主 `libpam` 在加载时提供:

```bash
nm -D --undefined-only pam_walletkey.so    # 期望只看到 pam_get_user / pam_set_item
```

2. 生成钱包密钥

```bash
openssl rand -base64 24 > wallet-key.txt
chmod 600 wallet-key.txt
```

这个文件在安装时会被装到 `/etc/pam_walletkey.key`(`root:root 0600`)。它不要进 Git、不要贴进文章、不要出现在任何脚本参数或日志里。

3. 把 KWallet 的钱包密码改成这个密钥

这一步要在图形界面里做,而且必须在安装脚本之前完成——否则 PAM 注入的密钥和钱包密码对不上,派生出的密钥打不开钱包,症状仍然是“弹框问密码”。

`kwalletmanager` → `kdewallet` → 更改密码 → 新密码两栏填入 `wallet-key.txt` 的内容。

4. 跑安装脚本

脚本把下面这些改动一次做完,并且是幂等的,重复运行不会重复插入:

```bash
#!/usr/bin/env bash
#
# biopass + sudo/polkit 人脸认证 + pam_walletkey 安装脚本
#
#   用法: sudo bash install.sh
#
# 安装内容:
#   1. /usr/lib/security/pam_walletkey.so        (自建 PAM 模块,把钱包密钥注入 PAM_AUTHTOK)
#   2. /etc/pam_walletkey.key                    (root:root 0600,钱包密码;需与 KWallet 密码一致)
#   3. /etc/pam.d/sudo                           (顶部插入 biopass sufficient 行)
#   4. /etc/pam.d/plasmalogin                    (pam_walletkey + pam_kwallet5 上移到 biopass 之前)
#   5. /etc/pam.d/polkit-1                       (polkit/pkexec 走 biopass)
#   6. /etc/systemd/system/polkit-agent-helper@.service.d/override.conf (放开摄像头设备)
#
# 幂等:可重复运行;每次运行都会把原文件备份到 /var/backups/biopass-kwallet-<时间戳>/
#
set -euo pipefail

STAMP="$(date +%Y%m%d-%H%M%S)"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
BACKUP_DIR="/var/backups/biopass-kwallet-${STAMP}"
SUDO_PAM="/etc/pam.d/sudo"
PLASMALOGIN_PAM="/etc/pam.d/plasmalogin"
POLKIT_PAM="/etc/pam.d/polkit-1"
OVERRIDE_DIR="/etc/systemd/system/polkit-agent-helper@.service.d"

if [[ "${EUID}" -ne 0 ]]; then
    echo "错误:请用 root 运行 → sudo bash \"${BASH_SOURCE[0]}\"" >&2
    exit 1
fi

need() {
    if [[ ! -e "${SCRIPT_DIR}/$1" ]]; then
        echo "错误:缺少交付文件 ${SCRIPT_DIR}/$1" >&2
        exit 1
    fi
}
need pam_walletkey.so
need wallet-key.txt
need plasmalogin.pam.new
need polkit-1.pam.new
need polkit-agent-helper-override.conf

echo "==> 备份到 ${BACKUP_DIR}"
mkdir -p "${BACKUP_DIR}"
chmod 700 "${BACKUP_DIR}"
for f in "${SUDO_PAM}" "${PLASMALOGIN_PAM}" "${POLKIT_PAM}"; do
    [[ -e "${f}" ]] && cp -a "${f}" "${BACKUP_DIR}/$(basename "${f}")"
done
[[ -d "${OVERRIDE_DIR}" ]] && cp -a "${OVERRIDE_DIR}" "${BACKUP_DIR}/"
echo "    完成后如出问题:sudo bash \"${SCRIPT_DIR}/rollback.sh\" ${BACKUP_DIR}"

echo "==> [1/6] 安装 pam_walletkey.so 与钱包密钥"
install -m 0755 -o root -g root "${SCRIPT_DIR}/pam_walletkey.so" /usr/lib/security/pam_walletkey.so
install -m 0600 -o root -g root "${SCRIPT_DIR}/wallet-key.txt" /etc/pam_walletkey.key
echo "    /etc/pam_walletkey.key 已就位(请确保它与 KWallet 钱包密码完全一致)"

echo "==> [2/6] 为 sudo 添加 biopass 认证(幂等)"
if grep -qE '^[[:space:]]*auth[[:space:]]+sufficient[[:space:]]+libbiopass_pam\.so' "${SUDO_PAM}"; then
    echo "    已存在,跳过"
else
    tmp="$(mktemp)"
    awk 'BEGIN{done=0}
         /^[[:space:]]*auth[[:space:]]/ && !done {
             print "auth\t\tsufficient\tlibbiopass_pam.so";
             print "# biopass: 人脸认证成功即免密;失败自动回退下方 system-auth 的密码认证";
             done=1
         }
         { print }' "${SUDO_PAM}" > "${tmp}"
    install -m 0644 -o root -g root "${tmp}" "${SUDO_PAM}"
    rm -f "${tmp}"
    echo "    已插入"
fi

echo "==> [3/6] 更新 plasmalogin 的 PAM 栈(pam_walletkey + pam_kwallet5 前置)"
install -m 0644 -o root -g root "${SCRIPT_DIR}/plasmalogin.pam.new" "${PLASMALOGIN_PAM}"

echo "==> [4/6] 让 polkit / pkexec 走 biopass"
install -m 0644 -o root -g root "${SCRIPT_DIR}/polkit-1.pam.new" "${POLKIT_PAM}"

echo "==> [5/6] 安装 polkit-agent-helper systemd override(放开摄像头设备)"
install -D -m 0644 -o root -g root "${SCRIPT_DIR}/polkit-agent-helper-override.conf" \
        "${OVERRIDE_DIR}/override.conf"

echo "==> [6/6] 重载 systemd 并重启 polkit"
systemctl daemon-reload
systemctl restart polkit.service

echo
echo "==> 完成。请在【新终端】中验证(本终端不要关):"
echo "    sudo -k; sudo true          # 预期:人脸识别通过 → 直接执行"
echo "    pkexec id                   # 预期:弹窗中出现 biopass 人脸识别"
echo
echo "  验证钱包:注销后用 biopass 登录,然后执行"
echo "    journalctl -b -u plasmalogin | grep -i pam_kwallet5"
echo "  预期不再出现 'open_session called without kwallet5_key'"
echo
echo "  回滚: sudo bash \"${SCRIPT_DIR}/rollback.sh\" ${BACKUP_DIR}"
```
```bash
sudo bash biopass-install.sh
```

它会在动手前把原文件备份到 `/var/backups/biopass-kwallet-<时间戳>/`。执行前先另开一个 root 终端放着别关。

登录路径:plasmalogin

`/etc/pam.d/plasmalogin` 是 greeter 登录用的 PAM 服务。`pam_walletkey` 和 `pam_kwallet5` 必须位于 biopass 的 `sufficient` 行之前,这是整件事的根因所在:

```ini
#%PAM-1.0

# ---- Biopass 人脸认证(Plasma Login Manager 专用;失败自动回退密码)----
# pam_walletkey(自建模块)先把 KWallet 密码从 /etc/pam_walletkey.key 注入 PAM_AUTHTOK,
# 再由 pam_kwallet5 存入 pam_set_data,session 阶段才能真正解锁 kdewallet。
# 因此这两行必须放在 biopass 的 sufficient 行之前(sufficient 成功会终止整个 auth 栈)。
-auth       optional    pam_walletkey.so
-auth       optional    pam_kwallet5.so
auth        sufficient  libbiopass_pam.so
auth        include     system-login
-auth       optional    pam_gnome_keyring.so

account     include     system-login

password    include     system-login
-password   optional    pam_gnome_keyring.so    use_authtok

session     optional    pam_keyinit.so          force revoke
session     include     system-login
-session    optional    pam_gnome_keyring.so    auto_start
-session    optional    pam_kwallet5.so         auto_start
```

auth 阶段注入的 `PAM_AUTHTOK` 会被 `pam_kwallet5` 保存下来,session 阶段才能拿到 `kwallet5_key`,然后解锁 `kdewallet`。

如果同时使用 PLM Autologin,`/etc/pam.d/plasmalogin-autologin` 也要放入同样的两行;那条路径上的 `pam_exec` 人脸闸门和 fail-closed 顺序见PLM 免按键人脸登录。

sudo

`sudo` 的改动只有一行,插在 `system-auth` 之前:

```ini
# /etc/pam.d/sudo
#%PAM-1.0
auth        sufficient  libbiopass_pam.so
# biopass:人脸认证成功即免密;失败自动回退下方 system-auth 的密码认证
auth        include     system-auth
account     include     system-auth
session     include     system-auth
session     optional    pam_systemd.so class=none
```

放在 `system-auth` 之前而不是改 `system-auth` 本身,影响面最小:只有 `sudo` 走人脸,`login`、`su`、`sshd` 全都不变。人脸成功即免密;人脸失败、取消或没有摄像头,都会正常回落到下面的密码提示。

`sudo` 的时间戳保持默认(15 分钟,按 tty 和父进程记)。第一次 `sudo -k; sudo true` 会做人脸认证,缓存有效期内的后续命令不会重复调用摄像头。

polkit 与 pkexec

polkit 在默认配置里是用不了人脸的两处都要改,只改 PAM 文件不够。

1. PAM 服务

`/etc/pam.d/polkit-1` 覆盖包内的 `/usr/lib/pam.d/polkit-1`(前者优先)。注意上游那份里本来就有 `password include system-auth`,改写时不要漏掉:

```ini
#%PAM-1.0

# polkit / pkexec 的 PAM 服务:先试 biopass 人脸,失败自动回退密码。
# 本文件覆盖 /usr/lib/pam.d/polkit-1(/etc/pam.d 优先级更高)。
# 注意:还需要 systemd override(polkit-agent-helper-override.conf)放开摄像头设备,
#       并在 biopass 配置里把 polkit-1 / pkexec 从 ignore_services 移除。
auth       sufficient  libbiopass_pam.so
auth       include     system-auth
account    include     system-auth
password   include     system-auth
session    include     system-auth
```

2. systemd drop-in:放开摄像头设备

这一条容易漏,但它决定成败。`polkit-agent-helper@.service` 默认带 `PrivateDevices=yes`,进程在自己的设备命名空间里看不到 `/dev/video*`。biopass 拿不到摄像头,人脸自然失败:

```ini
[Service]
# biopass 需要访问摄像头(polkit-agent-helper 默认 PrivateDevices=yes 会屏蔽 /dev/video*)
PrivateDevices=no
DevicePolicy=auto
DeviceAllow=char-video4linux rw
DeviceAllow=char-media rw
DeviceAllow=char-drm rw
DeviceAllow=/dev/uinput rw

# 允许 biopass 把日志写到 ~/.local/share/com.ticklab.biopass/logs/
ProtectHome=no
```

`ProtectHome=no` 是为了让 biopass 能往 `~/.local/share/com.ticklab.biopass/logs/` 写日志;不加也能认证,只是丢日志。

3. 让 biopass 别再忽略 polkit

biopass 默认把 polkit 相关服务排除在外,要把它从忽略列表里去掉。改的是用户自己的配置:

```bash
# ~/.config/com.ticklab.biopass/config.yaml
strategy:
  ignore_services: []
```

本机改动前这一项是 `[polkit-1, pkexec]`。改完重启 polkit:

```bash
sudo systemctl daemon-reload
sudo systemctl restart polkit.service
```

验证结果

每改一层就验证一层,出了问题容易回到上一步:

```bash
# sudo:人脸通过应成功,失败应回到密码
sudo -k
sudo true

# polkit:确认 pkexec 走的是同一套人脸配置
pkexec id

# 登录后的钱包状态
busctl --user call org.kde.kwalletd6 /modules/kwalletd6 \
  org.kde.KWallet isOpen s kdewallet
```

本机验证结果如下:

排错

通用性

回滚

回滚脚本会还原三份 PAM 文件、移除自建模块与密钥文件、删掉 polkit 的 override,并重启 polkit:

```bash
#!/usr/bin/env bash
#
# 回滚脚本:撤销 install.sh 的所有系统改动
#
#   用法: sudo bash rollback.sh [备份目录]
#          不给参数时自动使用 /var/backups/biopass-kwallet-* 中最新的一份
#
set -euo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
OVERRIDE_DIR="/etc/systemd/system/polkit-agent-helper@.service.d"

if [[ "${EUID}" -ne 0 ]]; then
    echo "错误:请用 root 运行 → sudo bash \"${BASH_SOURCE[0]}\"" >&2
    exit 1
fi

if [[ $# -ge 1 ]]; then
    BACKUP_DIR="$1"
else
    BACKUP_DIR="$(ls -1dt /var/backups/biopass-kwallet-* 2>/dev/null | head -n 1 || true)"
fi

if [[ -z "${BACKUP_DIR:-}" || ! -d "${BACKUP_DIR}" ]]; then
    echo "错误:找不到备份目录,请手动指定:sudo bash rollback.sh /var/backups/biopass-kwallet-YYYYmmdd-HHMMSS" >&2
    exit 1
fi

echo "==> 使用备份:${BACKUP_DIR}"

echo "==> 还原 PAM 配置"
for name in sudo plasmalogin polkit-1; do
    if [[ -e "${BACKUP_DIR}/${name}" ]]; then
        install -m 0644 -o root -g root "${BACKUP_DIR}/${name}" "/etc/pam.d/${name}"
        echo "    /etc/pam.d/${name} 已还原"
    elif [[ "${name}" == "polkit-1" ]]; then
        rm -f /etc/pam.d/polkit-1
        echo "    /etc/pam.d/polkit-1(本次新建)已删除,回落 /usr/lib/pam.d/polkit-1"
    fi
done

echo "==> 移除自建模块与钱包密钥"
rm -f /usr/lib/security/pam_walletkey.so
rm -f /etc/pam_walletkey.key

echo "==> 还原 polkit-agent-helper override"
if [[ -d "${BACKUP_DIR}/polkit-agent-helper@.service.d" ]]; then
    mkdir -p "${OVERRIDE_DIR}"
    cp -a "${BACKUP_DIR}/polkit-agent-helper@.service.d/." "${OVERRIDE_DIR}/"
    echo "    已还原原有 override"
else
    rm -rf "${OVERRIDE_DIR}"
    echo "    已删除本次新增的 override 目录"
fi

echo "==> 重载 systemd 并重启 polkit"
systemctl daemon-reload
systemctl restart polkit.service

cat <<'EOF'

==> 回滚完成。请注意两件事:
    1) KWallet 密码仍是安装时设置的随机密钥:请在 kwalletmanager 里把钱包密码改回你自己记得的值。
    2) biopass 的「忽略服务」列表请手动把 polkit-1 / pkexec 加回去
       (~/.config/com.ticklab.biopass/config.yaml 的 strategy.ignore_services)。
EOF
```
```bash
sudo bash biopass-rollback.sh              # 不给参数 = 用最新那份备份
```

跑完之后还有两件事脚本没法替你做,它在结尾也会提醒:

  1. KWallet 的密码仍是那串随机密钥,要在 `kwalletmanager` 里改回你自己记得的值;
  2. biopass 的忽略列表要把 `polkit-1` 和 `pkexec` 加回去(`~/.config/com.ticklab.biopass/config.yaml` 的 `strategy.ignore_services`)。

方案的安全边界很明确:root 能读到密钥,就能解锁这个钱包;普通用户不能读密钥文件。它保留了钱包文件的静态加密,但没有把 PAM 变成密钥管理系统。

长期维护

这次改动会在系统升级时被绕过,而且两类文件的表现不一样,处理方式也不同。

包自己拥有的文件会生成 `.pacnew`。 `/etc/pam.d/sudo` 属于 `sudo` 包,升级时 pacman 会把它另存为 `/etc/pam.d/sudo.pacnew`:新版本里没有我们插入的那一行,需要手动合回去。

我们自己新建的文件不会有 `.pacnew`,但上游默认值会变。 `/etc/pam.d/plasmalogin`、`/etc/pam.d/polkit-1`、`/etc/pam.d/plasmalogin-autologin` 都不属于任何包(它们的上游版本在 `/usr/lib/pam.d/` 下),所以 pacman 不会碰它们。代价是包更新后 `/usr/lib/pam.d/` 那份变了,我们的文件不会跟着变,也不会有人提醒。

```bash
# 1) 有没有 pacnew
sudo find /etc/pam.d -name '*.pacnew'

# 2) 我们改过的文件和上游默认值差在哪
for f in plasmalogin polkit-1 plasmalogin-autologin; do
    echo "--- $f"
    diff -u "/usr/lib/pam.d/$f" "/etc/pam.d/$f"
done
```

注意

`install.sh` 只对 `/etc/pam.d/sudo` 是“插入一行”的幂等操作;对另外三个文件是整份覆盖,不是合并。上游默认值如果新增了模块或参数,重跑脚本会把它们丢掉。所以升级后先 diff,再决定是重跑脚本还是手工把那几行合进去。

上面这些路径是 Arch 系的布局(上游默认放 `/usr/lib/pam.d/`)。别的发行版可能把上游默认直接放在 `/etc/pam.d/` 里,那时它们会产生 `.pacnew`,第 2 步的 `diff` 也就没有意义了。

参考

本文引用的文章

引用本文的文章