Duplicati 的 libsecret D-Bus 代码生成:Tmds.DBus 工具链与 Secret Service 集成实践

原创2026-10-02 20:56:461,442 阅读
文章标签:应用桌面应用CLI存储

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):

  1. 通过 DBusConnection.Session 建立会话总线连接;
  2. new DBusService(connection, serviceName) 绑定服务名(默认 org.freedesktop.secrets);
  3. service.OpenSessionAsync("plain", "") 打开明文会话,得到 session 路径;
  4. 解析集合路径(见下文);
  5. 用 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 密钥环接入问题,都能有的放矢。

登录后查看全文
duplicati