Noctalia 剪切板历史丢失修复:KeepassXC 替代 gnome-keyring
文档类型:这是基于特定版本和特定用户会话的排障记录,不是通用迁移脚本。迁移前必须备份 Noctalia 状态和 Secret Service 数据,并确认当前版本仍使用本文中的 schema 和配置路径。
风险:master key 可以解密已有剪切板历史。不要把 key 输出到日志、聊天记录或 Git 仓库;在验证回滚成功前,不要删除旧 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: 1、The 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
补充:导出后检查文件必须正好包含 64 个十六进制字符。若输出格式包含额外换行,可在副本上去除换行后再检查:
tr -d '\n' < ~/.local/state/noctalia/storage.key.bak > /tmp/noctalia-storage.key
mv /tmp/noctalia-storage.key ~/.local/state/noctalia/storage.key.bak
chmod 600 ~/.local/state/noctalia/storage.key.bak
test "$(wc -c < ~/.local/state/noctalia/storage.key.bak)" -eq 64
最后一个检查必须返回成功。若长度不是 64,说明 Secret Service 输出中可能包含额外换行或内容格式不符合预期,不要继续迁移。
即使后续方案失败,也只能在确认当前 Noctalia 版本支持 [storage] key_source = "file" 且配置路径正确时,使用 key 文件回退。
步骤 2:KeepassXC 启用 Secret Service 集成(两个开关!)
只开全局开关不够,新版 KeepassXC 要求每个数据库单独配置暴露组:
Settings → Secret Service Integration → Enable KeePassXC Freedesktop.org Secret Service integrationDatabase → 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:停止冲突的 Secret Service 提供者
org.freedesktop.secrets 是独占名字,gnome-keyring 不退出时 KeepassXC 无法接管。先停止或禁用产生冲突的 gnome-keyring 实例,并确认 KeepassXC 已经持有该 D-Bus 名字。
不要在验证迁移成功前卸载 gnome-keyring。卸载可能影响 Chrome、Git 凭据和其他依赖 Secret Service 的应用;旧 keyring 数据应保留到回滚确认完成。
步骤 4:迁移 master key(踩坑记录)
用 gdbus 调 org.freedesktop.Secret.Service.CreateItem 会报 UnknownInterface——KeepassXC 的 Service 接口没有 CreateItem(只有 OpenSession/CreateCollection/SearchItems/Unlock/Lock/GetSecrets/ReadAlias/SetAlias)。
正确入口在 Collection 接口。但是 Collection 路径、D-Bus Secret 结构和 32 字节二进制 value 都是运行时数据,不能把下面带有占位符的示意命令当作可执行命令。当前文章没有提供经过实际版本验证的迁移脚本;在脚本完成前,不要手写 gdbus 命令迁移二进制 key。
原文迁移命令(保留,仅作排障记录;包含运行时占位符,不能直接执行):
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"
在真正的迁移脚本中,至少需要完成:
- 读取并验证 master key 恰好为 32 字节。
- 动态发现 KeePassXC 暴露的 collection 路径。
- 使用 Secret Service 的 Collection 接口创建条目。
- 写入
dev.noctalia.Secretschema 和全部属性。 - 读取条目并与原 key 做字节级比较。
要点:
- 必须带
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 & 启动第二个实例。应使用原来的 Niri 配置、用户服务或桌面启动方式重启 Noctalia,并确认旧实例已经退出。
原文手动重启命令(保留,仅适用于确认 Noctalia 是手动启动且 PID 唯一的环境):
kill <noctalia-pid> && nohup noctalia &
验证点:
- 启动日志不再出现
storage master key is missing - 剪切板面板恢复历史条目
- 新复制的内容落盘到
entries/
回滚
在历史恢复成功前保留旧 keyring 和 storage.key.bak。迁移失败时按以下顺序回退:
先停止 Noctalia,避免它在 Secret Service 提供者切换期间继续读写:
systemctl --user stop noctalia.service如果 Noctalia 不是由该 user unit 管理,使用原来的会话管理方式停止对应实例,不要强制结束包含未保存状态的进程。
保存并退出 KeePassXC,并关闭 KeePassXC 的 Secret Service 集成。不要在数据库仍有未保存修改时强制结束进程。
确认当前用户存在 gnome-keyring user unit:
systemctl --user cat gnome-keyring-daemon.service如果 unit 存在,重新启动它:
systemctl --user restart gnome-keyring-daemon.service如果当前发行版没有该 unit,退出图形会话并重新登录,让 D-Bus activation 重新启动 gnome-keyring。
确认 Secret Service 名字已经由旧提供者接管:
busctl --user status org.freedesktop.secrets输出中的 owner 应为 gnome-keyring,而不是 KeePassXC。
不打印 master key 内容,只验证旧 keyring 是否可以读取:
secret-tool lookup application noctalia scope storage \ owner encrypted-state name master-key version 1 > /dev/null按原来的 Niri 配置、用户服务或桌面启动方式启动 Noctalia,并确认旧历史和新剪切板条目均正常。
如果第 5 步失败,先停止操作。不要直接用 secret-tool store 导入 storage.key.bak;Generic schema 可能无法满足 Noctalia 的自定义 schema 要求。确认旧 keyring 数据没有被删除后,再使用经过验证的迁移脚本处理。
降级场景(重要)
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
补充:也可以将 COLLECTION_PATH 替换为实际发现的 collection 对象路径:
gdbus introspect --session --dest org.freedesktop.secrets --object-path "$COLLECTION_PATH"
注意事项
- 停用 gnome-keyring 会影响依赖它的应用:Chrome 密码(
Chrome Safe Storage)、GitHub 凭据、部分 Flatpak 应用密钥等。数据文件保留,但服务不提供,相关应用需重新登录。 - KeepassXC 的 Secret Service 是只读优先实现:Service 接口没有
CreateItem(Collection 接口有),写入能力有限,读取/搜索是完整支持的。 - 备份的
storage.key.bak等于历史内容的解密钥匙,务必 600 权限妥善保管。