Noctalia 剪切板历史丢失修复:KeepassXC 替代 gnome-keyring

现象

Niri + Noctalia(基于 quickshell 的桌面 shell)自带剪切板管理器,但每次重启后剪切板历史全部丢失。面板打开后历史为空,仿佛从未复制过任何内容。

诊断过程

1. 剪切板历史是加密持久化的,不是内存态

Noctalia 的剪切板历史保存在:

~/.local/state/noctalia/clipboard/
├── index.enc          # 加密索引(477B,NOCTALIAENC1 头)
└── entries/           # 加密条目
    └── <时间戳>-1.enc

文件头 NOCTALIAENC1 表明使用 libsodium 加密。历史数据其实一直没丢,只是解不开。

2. 解密需要一把 master key,存在 Secret Service 里

Noctalia 用 32 字节 master key 派生各用途密钥(crypto_kdf_derive_from_key,context 区分 clipboard / calendar)。这把 master key 通过 libsecret 存到 Secret Service(gnome-keyring),schema 为 dev.noctalia.Secret,属性:

application = noctalia
scope       = storage
owner       = encrypted-state
name        = master-key
version     = 1

3. 启动日志暴露真相

每次启动日志都出现:

[WRN] [storage-key] encrypted cache/history exists, but its storage master key is missing

对应源码 lookupKey()NotFound 分支:加密数据存在,但 Secret Service 里查不到 master key → 进入 MissingKey 状态 → 历史无法解密,且新条目也不落盘(持久化禁用)→ 每次重启都"丢"。

4. 真正的根因:gnome-keyring 双实例 + 启动锁定

排查发现 master key 其实存在(用 secret-tool search 在 keyring 里找到了对应 item,md5 哈希属性完全匹配),但 noctalia 启动时读不到。原因有两层:

  • gnome-keyring 双实例冲突:systemd user service(gnome-keyring-daemon.service)与 dbus activation 实例(dbus-:1.1-org.freedesktop.secrets@0.service)并存,日志出现 discover_other_daemon: 1The Secret Service was already initialized。实际持有 org.freedesktop.secrets 的是 activation 实例。
  • 默认密钥环启动时处于 Locked 状态org.freedesktop.Secret.Collection.Locked = true,noctalia 带 SECRET_SEARCH_UNLOCK 解锁失败 → 搜不到 → 误报 NotFound。

gnome-keyring 被卸载/重装、keyring 文件重建等历史操作(本机 7 月起反复出现该警告)加剧了死锁:数据在、key 在、但服务状态异常导致永远读不到。

修复方案:KeepassXC 替代 gnome-keyring

与其修复脆弱的 gnome-keyring,不如让 KeepassXC 作为 Secret Service 提供者(用户已有的密码管理器,支持 FdoSecrets 集成)。

步骤 1:备份 master key(关键保险)

趁 keyring 解锁窗口,把 key 导出为 hex 文件:

secret-tool lookup application noctalia scope storage \
  owner encrypted-state name master-key version 1 \
  | xxd -p -c 100 > ~/.local/state/noctalia/storage.key.bak
chmod 600 ~/.local/state/noctalia/storage.key.bak

即使后续方案失败,也可用 Noctalia 的 [storage] key_source = "file" 模式回退(key 文件要求 64 位小写 hex,可选换行)。

步骤 2:KeepassXC 启用 Secret Service 集成(两个开关!)

只开全局开关不够,新版 KeepassXC 要求每个数据库单独配置暴露组

  1. Settings → Secret Service Integration → Enable KeePassXC Freedesktop.org Secret Service integration
  2. Database → Database Settings → Secret Service Integration → 勾选 Expose entries under this group 并选择组(否则该数据库不创建 collection,文档原话:“Collections are deleted if the exposed group is set to none”

验证服务接管:

busctl --user list | grep org.freedesktop.secrets   # owner 变成 keepassxc
gdbus call --session --dest org.freedesktop.secrets \
  --object-path /org/freedesktop/secrets \
  --method org.freedesktop.DBus.Properties.Get \
  org.freedesktop.Secret.Service Collections

步骤 3:卸载 gnome-keyring,释放 D-Bus 名字

org.freedesktop.secrets 是独占名字,gnome-keyring 不退出 KeepassXC 抢不到。卸载后 keyring 数据文件(~/.local/share/keyrings/)保留在磁盘上,可随时回滚。

步骤 4:迁移 master key(踩坑记录)

gdbusorg.freedesktop.Secret.Service.CreateItem 会报 UnknownInterface——KeepassXC 的 Service 接口没有 CreateItem(只有 OpenSession/CreateCollection/SearchItems/Unlock/Lock/GetSecrets/ReadAlias/SetAlias)。

正确入口在 Collection 接口

gdbus call --session \
  --dest org.freedesktop.secrets \
  --object-path /org/freedesktop/secrets/collection/<collection-id> \
  --method org.freedesktop.Secret.Collection.CreateItem \
  "{'org.freedesktop.Secret.Item.Label': <'Noctalia encrypted storage key'>, \
    'org.freedesktop.Secret.Item.Attributes': <{'application': 'noctalia', 'scope': 'storage', \
    'owner': 'encrypted-state', 'name': 'master-key', 'version': '1', \
    'xdg:schema': 'dev.noctalia.Secret'}>}" \
  "('', [], [bytearray([<32字节二进制>])])" \
  "true"

要点:

  • 必须带 xdg:schema: dev.noctalia.Secret 属性:Noctalia 用自定义 schema 搜索,libsecret 会把 schema 作为 xdg:schema 一起匹配;用 secret-tool store(Generic schema)存进去 Noctalia 搜不到。
  • value 是二进制 32 字节gdbus 解析嵌套 (oayay) 有缺陷(unknown keyword),可用 dbus-next 等库;KeepassXC 的 collection id 形如 _E5_AF_86_E7_A0_81(URL 编码的"密码")。
  • 密码字段存的是原始字节,不能手动在 GUI 里输 hex(会变成 64 字节 ASCII,Noctalia 校验 SecureKey::Size 会报 invalid size)。

步骤 5:重启 Noctalia 验证

kill <noctalia-pid> && nohup noctalia &

验证点:

  • 启动日志不再出现 storage master key is missing
  • 剪切板面板恢复历史条目
  • 新复制的内容落盘到 entries/

降级场景(重要)

KeepassXC 数据库锁定时 FdoSecrets 不公开 collection,Noctalia 会回到 Unavailable 状态。Noctalia 内置了恢复机制:监听 org.freedesktop.secrets 名字出现 → retrySecretServiceConsumers() → 重新 lookup。所以解锁 KeepassXC 后 Noctalia 能自动恢复,无需重启。代价是保持 KeepassXC 常驻解锁。

关键命令速查

# 谁持有 Secret Service 名字
busctl --user list | grep org.freedesktop.secrets

# 查看服务/集合暴露了哪些接口
gdbus introspect --session --dest org.freedesktop.secrets --object-path /org/freedesktop/secrets
gdbus introspect --session --dest org.freedesktop.secrets --object-path /org/freedesktop/secrets/collection/<id>

# 读 master key(验证迁移)
secret-tool lookup application noctalia scope storage owner encrypted-state name master-key version 1

# 查看 Noctalia 日志中的 storage-key 告警
grep "storage-key" ~/.cache/noctalia/noctalia.log

注意事项

  • 停用 gnome-keyring 会影响依赖它的应用:Chrome 密码(Chrome Safe Storage)、GitHub 凭据、部分 Flatpak 应用密钥等。数据文件保留,但服务不提供,相关应用需重新登录。
  • KeepassXC 的 Secret Service 是只读优先实现:Service 接口没有 CreateItem(Collection 接口有),写入能力有限,读取/搜索是完整支持的。
  • 备份的 storage.key.bak 等于历史内容的解密钥匙,务必 600 权限妥善保管。