Duplicati 的 libsecret D-Bus 代码生成:Tmds.DBus 工具链与 Secret Service 集成实践
Duplicati 的 libsecret D-Bus 代码生成:Tmds.DBus 工具链与 Secret Service 集成实践
导读
本文聚焦 Duplicati 在 Linux 平台上通过 freedesktop Secret Service API 接入 GNOME Keyring / KWallet 等密钥环的实现基石——secrets.DBus.cs 的自动生成流程。你将掌握如何使用 Tmds.DBus.Tool 安装代码生成工具、理解生成产物的文件构成与不可手工编辑的原因、通过一条 dotnet-dbus 命令完成代理类的再生成,并深入看到这些代理类在 Duplicati 密钥提供器(LibSecretLinuxProvider)与密钥集合(SecretCollection)中的实际调用关系,为二次开发或调试 Linux 密钥环集成提供可直接复用的操作手册。
背景:为什么 Duplicati 需要生成的 D-Bus 代理代码
Duplicati 的架构中,密钥(Secret)可以来自多种来源:环境变量、JSON 文件、AWS Secrets Manager、HashiCorp Vault、GCS、Azure Key Vault、macOS Keychain、Windows Credential Manager,以及 Linux 上的 Unix pass 与 libsecret。在 SecretProviderModules.cs 中可以看到,LibSecretLinuxProvider 仅在 OperatingSystem.IsLinux() 时为真时才会被注册进内置模块列表。
libsecret 并不是一个独立的库实现,而是 Linux 桌面环境(GNOME Keyring、KWallet 等)通过 D-Bus 会话总线暴露的 org.freedesktop.secrets 服务。C# 程序要与该服务通信,最可靠的方式是使用代理(Proxy)类封装 D-Bus 方法调用。这些代理类通常由 Tmds.DBus.Tool 自动生成——这正是本 README 的核心主题。
由 LibSecretLinuxProvider.cs 可知,默认的 D-Bus 服务名为 org.freedesktop.secrets;而 secrets.DBus.cs 文件头的生成标记证明了它的来源:
// Generated by Tmds.DBus.Tool v0.95.0 with arguments: codegen --protocol-api
// --service org.freedesktop.secrets --namespace secrets.DBus
// org.freedesktop.Secret.Prompt.xml --output secrets.DBus.cs
因此,理解代码生成流程,等同于理解 Duplicati Linux 密钥环集成的"上游源头"。
文件组成:一个 XML 补丁 + 一份生成产物
LibSecret 目录下包含三个与代码生成直接相关的文件:
| 文件 | 作用 |
|---|---|
| secrets.DBus.cs | 由 Tmds.DBus.Tool 生成的 D-Bus 代理类,覆盖 freedesktop Secret Service API(服务端、会话、集合、条目、提示框等代理类型),约 1600+ 行 |
| org.freedesktop.Secret.Prompt.xml | org.freedesktop.Secret.Prompt 接口的 D-Bus introspection XML,用于弥补实时总线无法发现的动态对象 |
| README.md | 本文所基于的代码生成说明文档 |
README 特别强调:不要手工编辑 secrets.DBus.cs,因为任何手动改动都会在下次重新生成时被覆盖。正确的维护方式是"改 XML / 改生成命令 → 重新运行 codegen"。
为什么需要手工维护 Prompt 的 XML
org.freedesktop.Secret.Prompt 是一个典型的运行时动态对象:只有当解锁集合、创建集合等操作需要用户交互确认时,Secret Service 才会在 /org/freedesktop/secrets/prompt/... 路径下临时创建 Prompt 对象。由于它不在服务的常规 introspection 数据中,dotnet-dbus 无法从活动总线自动发现它,因此必须通过 XML 文件显式声明。
该 XML 完整声明了 Prompt 接口的三类成员(org.freedesktop.Secret.Prompt.xml):
- 方法
Prompt(window_id: s):请求 Secret Service 弹出用户界面窗口; - 方法
Dismiss():关闭提示框; - 信号
Completed(dismissed: b, result: v):携带用户"取消"标记(布尔值)与操作结果(变体类型,通常是对象路径)。
这份 XML 恰好对应 SecretCollection.cs 中的异步等待逻辑:先 CreatePrompt(promptPath)、订阅 WatchCompletedAsync 完成信号,再调用 PromptAsync 弹出界面,最后通过 Task.WhenAny 与 120 秒超时兜底等待结果。
安装代码生成工具
README 给出的安装命令是全局安装 Tmds.DBus.Tool:
dotnet tool install --global Tmds.DBus.Tool
这是 .NET 标准的全局工具安装方式,安装后命令名为 dotnet-dbus。其版本应与生成产物头部记录的工具版本(当前仓库中为 v0.95.0)保持一致,以降低生成结果的漂移风险。若要固定到特定版本,可以使用带版本号的安装形式:
dotnet tool install --global Tmds.DBus.Tool --version 0.95.0
安装完成后可以通过 dotnet-dbus --help 验证工具可用,并确认 codegen 子命令及其参数(--protocol-api、--service、--namespace、--output)与 README 示例一致。
重新生成代理代码:完整命令与执行前提
README 给出了从 LibSecret 目录执行的重新生成命令:
dotnet-dbus codegen --protocol-api --service org.freedesktop.secrets --namespace secrets.DBus org.freedesktop.Secret.Prompt.xml --output secrets.DBus.cs
各参数含义如下:
| 参数 | 含义 |
|---|---|
codegen |
调用代码生成子命令 |
--protocol-api |
生成"协议 API"风格的代理类(与 secrets.DBus.cs 现有代码风格一致) |
--service org.freedesktop.secrets |
指定要 introspection 的 D-Bus 服务名,即 freedesktop Secret Service 的默认服务名 |
--namespace secrets.DBus |
生成代码的命名空间,与 SecretCollection.cs 中 using secrets.DBus; 对应 |
org.freedesktop.Secret.Prompt.xml |
额外的 introspection XML,用于补充动态 Prompt 对象 |
--output secrets.DBus.cs |
输出文件路径 |
命令的执行过程可以概括为两步:先对会话总线上的 org.freedesktop.secrets 服务做实时 introspection,获取静态接口;再与 XML 中声明的 Prompt 接口合并,生成完整的代理类集合。
关键前提:必须有正在运行的 Secret Service
README 的 Note 强调了执行环境要求:
A running secret service (e.g., GNOME Keyring or KWallet) must be available on the session bus for the introspection to work.
也就是说,只有在图形会话中已有 GNOME Keyring 或 KWallet 等密钥环服务监听 org.freedesktop.secrets 时,introspection 才能成功。这与运行时的一致性检查相互印证:在 SecretCollection.cs 的 IsSupported 中,Duplicati 除了要求 OperatingSystem.IsLinux(),还会检查 DISPLAY / WAYLAND_DISPLAY 环境变量(判定是否存在图形会话),随后以 5 秒超时探测 GetCollectionsAsync();任一环节失败都视为不支持。也就是说,"无图形会话 + 无密钥环服务"时,libsecret 提供器会被自动判定为不可用。
深入源码:生成的代理类如何支撑实际运行
连接与初始化链路
LibSecretLinuxProvider 的 InitializeAsync(LibSecretLinuxProvider.cs)只做两件事:用查询字符串解析配置,然后调用 SecretCollection.CreateAsync 并 UnlockAsync。真正的 D-Bus 连接发生在 SecretCollection.CreateAsync(SecretCollection.cs):
- 通过
DBusConnection.Session建立会话总线连接; new DBusService(connection, serviceName)绑定服务名(默认org.freedesktop.secrets);service.OpenSessionAsync("plain", "")打开明文会话,得到 session 路径;- 解析集合路径(见下文);
- 用
GetLockedAsync()判断集合是否锁定,随后由UnlockAsync()处理解锁(含 30 秒提示框超时兜底,SecretCollection.cs)。
集合路径解析的两种策略
GetCollectionPath(SecretCollection.cs)展示了"默认集合"的解析规则,这一点对使用者配置参数非常关键:
- collection 名为空:先尝试通过
ReadAliasAsync("default")解析 Secret Service 的default别名;若别名未设置,则回退匹配名为login的集合(login集合会在用户登录时自动解锁,见常量DefaultCollectionActualName,SecretCollection.cs); - collection 名为显式值:枚举
GetCollectionsAsync()结果,按对象路径的最后一个路径段精确匹配(例如请求login不会误中mylogin,见 SecretCollection.cs)。
读写密钥时的代理调用
- 写入:
StoreSecretAsync(SecretCollection.cs)在更新已存在条目时调用item.SetSecretAsync,新建条目时调用collection.CreateItemAsync,并写入xdg:schema=com.duplicati.Secret、server=Duplicati Secrets、user=<label>、type=plaintext等属性,兼顾 KDE KeepSecret 按server分组与 Seahorse 按user展示的习惯; - 读取:
GetSecretsAsync(SecretCollection.cs)先通过SearchItemsAsync枚举条目,再逐个GetLabelAsync匹配标签,最后item.GetSecretAsync(_session.Path)取回密文;任何缺失标签都会抛出MissingSecrets用户可读异常。
提供器级配置参数
LibSecretConfig(LibSecretLinuxProvider.cs)暴露了四个可通过 URI 查询串配置的参数:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
collection |
String | 空(解析默认集合) | 使用的集合(密钥环)名,留空走系统默认(default 别名 → login) |
case-sensitive |
Boolean | false | 标签匹配是否区分大小写;false 时使用 StringComparer.OrdinalIgnoreCase |
no-autocreate-collection |
Boolean | false | 为 true 时禁止在集合不存在时自动创建(存储场景会抛 CollectionNotFound) |
service-name |
String | org.freedesktop.secrets |
实现 freedesktop Secret Service API 的 D-Bus 服务名 |
对应的使用示例形如:
libsecret://?collection=login&case-sensitive=true&no-autocreate-collection=false
提供器在 Duplicati 主流程中的接入方式
SecretProviderModules 将 LibSecretLinuxProvider 注册进模块表后,由 SecretProviderHelper.cs 的 ApplySecretProviderAsync 统一消费:用户通过 --secret-provider=libsecret://... 指定提供器,SecretProviderLoader.CreateInstanceAsync 完成实例化,随后 ReplaceSecretsAsync(SecretProviderHelper.cs)按 secret-provider-pattern(默认 $,即 $<key> 语法)扫描所有选项值与 URI 查询参数,把匹配的占位符批量替换为从 libsecret 解析出的真实值,并且支持 secret-provider-cache(None / InMemory / Persistent 三级缓存,持久化缓存以 secret-cache-*.json.aes 的 AESCrypt 加密文件落盘,见 SecretProviderHelper.cs)。这意味着 $ 前缀的占位符可以在备份命令的任意参数(如后端口令、密钥口令)中使用,由 libsecret 统一托管。
测试覆盖与验证
单元测试 SecretProviderSetSecretTests.cs 验证了提供器接口的写入语义(对 FileProvider 的 SetSecretAsync 覆盖了"写入 → 重名拒绝(overwrite: false 抛 UserInformationException)→ 覆盖更新"的完整路径)。虽然 libsecret 的 D-Bus 交互依赖真实图形会话,难以在无头 CI 中端到端复现,但 SecretCollection.IsSupported 与 CollectionExists 等无侵入探测方法为运行时的可用性判定与降级提供了依据。若需在本地验证代码生成结果,可以:先确认图形会话中 secret-tool(GNOME Keyring 的命令行工具)可用,再用上文的 dotnet-dbus 命令重新生成并 git diff 对比新旧 secrets.DBus.cs。
常见问题与维护要点
- 手工改动
secrets.DBus.cs会被覆盖:所有针对代理层的定制都应落在 XML 或生成命令上,而不是直接编辑生成文件; - 无头服务器(SSH 会话)上不可用:没有
DISPLAY/WAYLAND_DISPLAY时,IsSupported直接返回 false,这与代码生成时"必须存在运行中的密钥环服务"的前提一致;此类环境应改用文件或环境变量提供器; - 解锁提示框超时:创建/解锁集合的交互提示分别有 120 秒与 30 秒的超时兜底,超时即抛出带
CreateCollectionPromptTimeout/UnlockPromptTimeout错误码的用户可读异常,便于排查"密钥环无法弹窗"的场景; - 升级工具版本后请重新生成:生成产物头部记录了工具版本,升级
Tmds.DBus.Tool后应重新运行 codegen,使代理类与新版协议 API 保持一致。
小结
LibSecret/README.md 所描述的代码生成流程,是 Duplicati Linux 密钥环集成的"源头工程":Tmds.DBus.Tool 负责从活动总线 + Prompt XML 生成不可手改的代理类,而 SecretCollection.cs 与 LibSecretLinuxProvider.cs 则把这些代理类转化为集合解析、锁定/解锁、密钥读写等可用的运行时能力。掌握这一工具链,无论是升级 D-Bus 协议 API、新增自定义接口,还是排查 Linux 密钥环接入问题,都能有的放矢。