人脸登录后的 KWallet 自动解锁
人脸认证证明了用户身份,却不会产生登录密码,因此标准 pam_kwallet5 拿不到 PAM_AUTHTOK。用一个只读密钥 PAM 模块把钱包密钥交给认证栈,保留 KWallet 加密并覆盖 sudo、polkit 和两条登录路径。
被修正 本文已被《被人脸钱包密钥掉包的登录密码》修正,见文末。另有 1 篇提到本文,见文末。
在很长一段时间里,虽然我已经利用 `biopass` 实现了人脸识别登录,但是我的使用率却相当低。虽然人脸已经识别成功,桌面也进去了,但是 KWallet 却弹出解锁框提示输入密码。即便把 `pam_kwallet5.so` 往前挪一行,通常也不会解决问题。
先决条件
```textCachyOS(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 阶段,但紧接着是:
```textpam_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 阶段——拿密码,存起来:
- `
pam_get_item(PAM_AUTHTOK)` 取当前认证口令; - 为空的话它会尝试自己弹窗问一次;
- 还是空,就直接返回 `
PAM_IGNORE`,源码里的日志原文是 `Empty or missing password, doing nothing`; - 拿到了,就用 `
pam_set_data("kwallet5_key", 密码)` 把它挂在 PAM handle 上。
session 阶段(配置行里的 `auto_start`)——用密码开钱包:
- 把上一步的 `
kwallet5_key` 取出来; - 拿 `
~/.local/share/kwalletd/kdewallet.salt` 当盐,做 PBKDF2(SHA512,50000 轮,输出 56 字节)派生钱包密钥; - 以用户身份拉起 `
ksecretd --pam-login <管道> <套接字>`,把密钥写进管道,完成解锁。
关键在第一步和第二步之间的唯一桥梁就是那串密码。也就是说,钱包的密码必须等于 `PAM_AUTHTOK` 里的东西,否则派生出来的密钥打不开钱包文件。这是后面所有方案的出发点。
钱包为什么拿不到密码
`sufficient` 会把后面的模块整组跳过
`man pam.conf` 对 `sufficient` 的定义是:该模块成功时,如果前面没有 required 模块失败,PAM 立即向应用返回成功,跳过本组剩余模块。
本机原始的授权栈顺序是:
- `
auth sufficient libbiopass_pam.so` ← 人脸成功,授权栈就地结束 - `
auth include system-login`(`pam_unix` 密码) - `
-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 密码、浏览器登录态和令牌。
第三种方案的三条设计边界:
- 密钥和登录密码不是同一个值。 钱包改用一个随机长串,登录密码照旧。
- 密钥只存在于 root 可读的文件里。 普通用户读不到,模块本身也不打印它。
- 模块读不到密钥时返回 `
PAM_IGNORE`,钱包回到手动解锁。 不能因为钱包配置错误把用户锁在登录界面外。
自建模块做什么
模块只做一件事:把一个受保护文件的第一行写进 `PAM_AUTHTOK`。它要满足的校验条件:
- 路径存在,而且是普通文件(不是符号链接、不是目录);
- 属主必须是 root;
- group 和 other 不能有任何权限位;
- 内容不能为空;
- 写进 PAM 之后立刻清掉本地临时缓冲区。
任何一条不满足,就记一条警告日志并返回 `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; }
```
几处实现细节:
- `
S_ISREG` 检查挡住了符号链接和特殊文件。 少了它,攻击者可以用一个指向 `/dev/zero` 或别人可写文件的路径把密钥内容变成别的字符串。 - `
st_uid != 0` 加权限位检查缺一不可。 只查属主不查权限,一个 `0644` 的 root 文件仍然人人可读。 - `
getpwnam` 跳过 root 用户。 root 不需要解锁钱包,让它也走一遍只会多一次无谓的文件读取。 - `
pam_get_user` 失败就返回。 拿不到用户名说明 PAM 上下文不对,此时继续没有意义。 - `
wipe()` 用 `volatile char *` 写零。 不加 `volatile`,编译器有权把这段“写了没人读”的循环优化掉,密钥就会留在栈上。 - `
syslog` 只打返回码,不打密钥。 日志会进 journal,任何会记录密钥内容的东西都不该出现在这里。
`pam_sm_setcred` 是个空实现,直接返回 `PAM_SUCCESS`。PAM 加载模块时会找这个符号,不提供会报错,但本方案在 setcred 阶段确实无事可做。
安装
分三步:编译模块、生成密钥、跑安装脚本。
1. 编译模块
把上面的 `pam_walletkey.c` 存到某个目录,然后:
```bashgcc -shared -fPIC -O2 -o pam_walletkey.so pam_walletkey.c
```
不需要 `-lpam`。本机实测编译出的 `.so` 里未定义的符号只有 `pam_get_user` 和 `pam_set_item` 两个 PAM 符号(其余都是 libc),它们由宿主 `libpam` 在加载时提供:
```bashnm -D --undefined-only pam_walletkey.so # 期望只看到 pam_get_user / pam_set_item
```
2. 生成钱包密钥
```bashopenssl 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}"
```
```bashsudo 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:
```bashsudo 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
```
本机验证结果如下:
- `
sudo` 人脸通过时无需密码,未识别时回退密码;TTY(`Ctrl+Alt+F3`)下也能用; - `
pkexec id` 返回 root 身份(`uid=0`),未输入密码; - autologin 和 greeter 两条登录路径都执行了 `
pam_kwallet5` 的 auth 和 session 阶段; - 日志中不再出现 `
open_session called without kwallet5_key`(0 条),取而代之的是 `pam_sm_authenticate` 加 `pam_sm_open_session`,且 `PAM_KWALLET5_LOGIN=/run/user/<uid>/kwallet5.socket` 与套接字都存在; - `
isOpen("kdewallet")` 返回 `true`,整个登录过程没有再次输入钱包密码。
排错
- 钱包还是不自动解锁,日志里仍有 `
without kwallet5_key`- 可能原因:`
pam_walletkey` 和 `pam_kwallet5` 排在了 `libbiopass_pam.so` 之后,`sufficient` 成功就跳过了它们。 - 怎么办:`
grep -nE 'pam_walletkey|pam_kwallet5|libbiopass' /etc/pam.d/plasmalogin`,确认顺序是 walletkey → kwallet5 → biopass。
- 可能原因:`
- 密钥对了,但钱包还是打不开
- 可能原因:KWallet 里的钱包密码和 `
/etc/pam_walletkey.key` 的内容不一致(哪怕差一个换行)。 - 怎么办:模块会剥掉行尾的 `
\r`/`\n`,但不会 trim 空格。重设一次钱包密码,确保是同一串字符。
- 可能原因:KWallet 里的钱包密码和 `
- 日志里出现 `
pam_walletkey: key file rejected (rc=-2)`- 可能原因:`
/etc/pam_walletkey.key` 的属主或权限不对(必须是 `root:root 0600`)。 - 怎么办:`
ls -l /etc/pam_walletkey.key`,然后用 `install -m 0600 -o root -g root` 重装一次。
- 可能原因:`
- 按网上说法加了 `
kwalletd=/usr/bin/ksecretd` 参数,没变化- 可能原因:本机 kwallet-pam 6.7.5 编译时的默认守护进程已经是 `
/usr/bin/ksecretd`,这个参数是多余的。 - 怎么办:去掉它。要紧的是钱包名 `
kdewallet` 和钱包密码。
- 可能原因:本机 kwallet-pam 6.7.5 编译时的默认守护进程已经是 `
- `
pkexec` 还是弹密码框- 可能原因:三处改动少了一处——PAM 文件、systemd drop-in、biopass 的忽略列表。
- 怎么办:`
systemctl show polkit-agent-helper@.service -p PrivateDevices` 应返回 `no`;再确认 `ignore_services` 里没有 `polkit-1`、`pkexec`。
- `
sudo` 变慢了- 可能原因:人脸单次判定约 2 秒;第一次没在 2 秒内正对镜头就会回落到密码。
- 怎么办:在 biopass 图形界面里调大 `
methods.face.retries` 或 `retry_delay`。终端里的 `sudo` 走 biopass 时没有摄像头预览窗口,看不到“正在识别”的反馈,这是第一次容易失败的主要原因。
通用性
- “人脸不产生密码”这条限制 —— ✅ 所有走 PAM 的生物识别模块都一样。
- `
sufficient` 会跳过后续模块 —— ✅ Linux-PAM 的固定语义。 - 模块的校验逻辑与 `
PAM_IGNORE` 兜底 —— ✅ 与发行版无关。 - 钱包密码必须等于注入的密钥 —— ✅ 硬性要求,任何方案都绕不过。
- polkit 需要 systemd drop-in —— ✅ `
PrivateDevices=yes` 是上游默认值。 - 钱包名、盐文件路径、PBKDF2 参数 —— ⚠️ 由具体的 pam_kwallet 版本决定,换版本要复核。
- 默认守护进程是 `
ksecretd` —— ⚠️ kwallet-pam 6.7.5 的编译默认值,更早的版本可能是 `kwalletd5`。 - `
/etc/pam.d/…` 的具体路径与内容 —— ⚠️ 发行版差异;本机是 Arch 系的 `system-auth` / `system-login` 体系。
回滚
回滚脚本会还原三份 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
```
```bashsudo bash biopass-rollback.sh # 不给参数 = 用最新那份备份
```
跑完之后还有两件事脚本没法替你做,它在结尾也会提醒:
- KWallet 的密码仍是那串随机密钥,要在 `
kwalletmanager` 里改回你自己记得的值; - 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` 也就没有意义了。
参考
- KDE Wallet:Arch Wiki
- kwallet-pam 源码
- biopass PAM 文档
- biopass Polkit 文档
- Linux-PAM 调度实现
- KDE bug 468069(空钱包密码 + 无 PAM 凭据时不自动打开)
本文引用的文章
背景 PLM 免按键人脸登录
引用本文的文章
被修正 被人脸钱包密钥掉包的登录密码
钱包两行已被移回 sufficient 之后,并新增一行 session 模块
背景 PLM 免按键人脸登录