RFC-0179:基本的剪贴板服务

RFC-0179:基本剪贴板服务
状态已接受
区域
  • HCI
说明

提议提供一个基本的剪贴板服务,以便用户在组件之间安全地复制和粘贴文本内容,而无需考虑 runner。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2022-05-16
审核日期(年-月-日)2020-07-18

摘要

此 RFC 引入了两个新的框架提供的协议(fuchsia.ui.clipboard.Writer 和 fuchsia.ui.clipboard.Reader)以及将实现这些协议的服务,这些协议和服务将允许用户对文本内容执行复制和粘贴操作。

设计初衷

许多具有图形界面的现代面向用户的操作系统都提供剪贴板功能(参见在先技术),允许用户以交互方式将数据复制到系统提供的内存缓冲区或其他渠道,并在稍后将该数据粘贴到其他位置。

过去,Fuchsia 曾实现过一个作为模块化代理的基本剪贴板协议,但此代码已于 2019 年被移除。

在此 RFC 中,我们建议引入一种新的剪贴板协议和实现,Fuchsia 产品可以选择集成该协议和实现。最迫切的需求是能够复制和粘贴 Unicode 文本,因此这将是第一个迭代版本的重点。

许多现有操作系统的剪贴板功能最初设计时没有安全措施,因此任何进程都可以在用户不知情或无意的情况下随时观察和/或修改剪贴板。在 Fuchsia 上,我们旨在通过以下方式在设计时就考虑到安全性:

  • 通过精细的功能来保护剪贴板访问权限,遵循最小权限原则
  • 尝试限制剪贴板访问权限,但前提是前台窗口(在 Fuchsia 的 Scenic 术语中为“View”)具有输入焦点
  • 仅在万不得已时才提供后台剪贴板访问权限

利益相关方

主持人

davemoore@google.com

审核人

Fuchsia HCI:neelsa@google.com、quiche@google.com

安全性:palmer@google.com

隐私权:enoharemaien@google.com

Chromium:wez@google.com

Flutter:jmccandless@google.com

Consulted

azaslavsky、carolineliu@google.com、chaopeng@google.com、cpu@google.com、ddorwin@google.com、fmil@google.com、jsankey@google.com、tjdetwiler@google.com

社会化

  • 在 Fuchsia 输入团队文档审核中讨论过
  • 在 Fuchsia 安全咨询交流时间中讨论过

设计

访问权限级别

图形 shell 环境中剪贴板的访问权限范围可分为三个级别:

  1. 由 Shell 介导、依赖于焦点
    组件只有在响应图形 Shell 确定的明确用户操作时,并且仅当组件当前具有输入焦点时,才能访问剪贴板。
  2. 依赖于焦点
    组件可以在获得输入焦点时随时访问剪贴板。
  3. 不受限
    组件可以随时访问剪贴板。

在此 RFC 中,我们仅介绍(2) 依赖于焦点的范围。

目前尚未计划设计和实现范围 (1) 和 (3);这些范围将需要另一份 RFC。

用例

对于初始 RFC,我们考虑了以下几个简单但常见的用例:

  • 在网络浏览器中,将网址从网页正文复制到地址栏
  • 将 shell 命令从网络浏览器复制到终端
  • 将信息从 Web 浏览器复制到工作站产品的反馈对话框(以 Flutter 实现)

协议和服务

我们在合作伙伴 SDK 中引入了两个新的可发现 FIDL 协议,即 fuchsia.ui.clipboard.FocusedReaderRegistry 和 fuchsia.ui.clipboard.FocusedWriterRegistry。这些协议将由在会话 realm 中运行的新组件 clipboard.cm 实现和公开。该组件将包含在工作站产品中,并且可用于需要它的任何其他 Fuchsia 产品。

获得 FocusedWriterRegistry 和 FocusedReaderRegistry 功能的客户端组件将能够分别请求 fuchsia.ui.clipboard.Writer 和 fuchsia.ui.clipboard.Reader 的实例。它们可以在任何时间请求这些连接(假设它们拥有有效的 ViewRef),但如果客户端的视图没有输入焦点,Writer 和 Reader 的方法将返回错误。

library fuchsia.ui.clipboard;

/// A protocol that allows graphical clients that own
/// [`ViewRef`s](https://cs.opensource.google/fuchsia/fuchsia/+/main:/src/development/graphics/scenic/concepts/view_ref) to request read ("paste")
/// access to the clipboard. Clients can register for access at any time, but `GetItem` calls will
/// only succeed while the view has input focus.
@discoverable
protocol FocusedReaderRegistry {
    /// If the `ViewRef` is valid, the clipboard server will allow the client to send commands using
    /// the given `Reader`. If the `ViewRef` later becomes invalid, the `Reader`'s channel will be
    /// closed.
    RequestReader(resource table {
        1: view_ref fuchsia.ui.views.ViewRef;
        2: reader_request server_end:Reader;
    }) -> (table {}) error ClipboardError;
};

/// A protocol that allows graphical clients that own `ViewRef`s to request write ("copy") access to
/// the clipboard. Clients can register for access at any time, but `SetItem` calls will only
/// succeed while the view has input focus.
@discoverable
protocol FocusedWriterRegistry {
    /// If the `ViewRef` is valid, the clipboard server will allow the client to send commands using
    /// the given `Writer`. If the `ViewRef` later becomes invalid, the `Writer`'s channel will be
    /// closed.
    RequestWriter(resource table {
        1: view_ref fuchsia.ui.views.ViewRef;
        2: writer_request server_end:Writer;
    }) -> (table {}) error ClipboardError;
};

/// Allows data to be read from the clipboard, i.e. pasted.
protocol Reader {
    /// Reads a single item from the clipboard. If the client's `View` does not have input focus, an
    /// error will be returned. If there is no item on the clipboard, `ClipboardError.EMPTY` will
    /// be returned.
    GetItem(table {}) -> (ClipboardItem) error ClipboardError;
};

/// Allows data to be written to the clipboard, i.e. copied.
protocol Writer {
    /// Writes a single item to the clipboard. If the client's `View` does not have input focus, an
    /// error will be returned.
    SetItem(ClipboardItem) -> (table {}) error ClipboardError;

    /// Clears the contents of the clipboard. If the client's `View` does not have input focus, an
    /// error will be returned.
    Clear(table {}) -> (table {}) error ClipboardError;
};

/// Set of errors that can be returned by the clipboard server.
type ClipboardError = flexible enum {
    /// An internal error occurred. All the client can do is try again later.
    INTERNAL = 1;

    /// The clipboard was empty, or the requested item(s) were not present on the clipboard.
    EMPTY = 2;

    /// The client sent an invalid request, e.g. missing requiring fields.
    INVALID_REQUEST = 3;

    /// The client sent the server an invalid `ViewRef` or a `ViewRef` that is already associated
    /// with another client.
    INVALID_VIEW_REF = 4;

    /// The client attempted to perform an operation that requires input focus, at a moment when
    /// it did not have input focus. The client should wait until it has focus again before
    /// retrying.
    UNAUTHORIZED = 5;
};

在初始版本中,剪贴板将仅支持复制和粘贴大小不超过 32 KB 的 UTF-8 字符串。客户端可以为数据指定 MIME 类型;默认值为 "text/plain;charset=UTF-8"。

后续修订版本将添加对 VMO 的支持,从而能够复制和粘贴任意数据。

/// The maximum length of a plain-text clipboard item in bytes. Although FIDL messages support
/// larger messages, this limit allows space to be reserved for potential other fields in the
/// message. Larger payloads will be supported by VMOs in `ClipboardItemData` in future revisions.
const MAX_TEXT_LENGTH uint32 = 32768;

/// The maximum length of a MIME Type identifier. Per
/// [IETF RFC 4288](https://datatracker.ietf.org/doc/html/rfc4288#section-4.2), a MIME type may have
/// up to 127 characters before and 127 characters after the slash, for a total of 255.
const MAX_MIME_TYPE_LENGTH uint32 = 255;

/// A single item on the clipboard, consisting of a MIME type hint and a payload.
type ClipboardItem = resource table {
    /// MIME type of the data, according to the client that placed the data on the clipboard.
    /// *Note:* The clipboard service does not validate clipboard items and does not guarantee that
    /// they conform to the given MIME type's specifications.
    1: mime_type_hint string:MAX_MIME_TYPE_LENGTH;
    /// The payload of the clipboard item.
    2: payload ClipboardItemData;
};

/// The payload of a `ClipboardItem`. Future expansions will support additional transport formats.
type ClipboardItemData = flexible resource union {
    /// A UTF-8 string.
    1: text string:MAX_TEXT_LENGTH;
};

实现

这一过程分为几个阶段:

  1. 提交新的 fuchsia.ui.clipboard FIDL 库(如上文预览所示)以供 API 审核。
  2. 实现一个新的剪贴板服务器组件,该组件在会话 realm 中运行,并公开 fuchsia.ui.clipboard.FocusedWriterRegistry 和 fuchsia.ui.clipboard.FocusedReaderRegistry 协议。
  3. 通过一个管理 Scenic 视图的简单组件,演示与新协议的集成。
  4. 将对新协议的支持集成到 Chromium 和 Flutter runner 中。

性能

添加新服务会占用额外的存储空间来存储二进制文件,还会占用内存来存储二进制文件和剪贴板内容。每个注册剪贴板访问权限的客户端都会通过保持 Zircon 通道处于打开状态来消耗资源。

安全注意事项

必须进行安全审核。

跨组件通信

剪贴板服务的引入构成了一个新的跨组件通信渠道。这为组件有意或无意地利用彼此的漏洞开辟了新的可能性。

不受信任的内容

剪贴板服务不保证 ClipboardItem 数据或 MIME 类型提示的可信度。因此,客户端不应信任其接收的内容,而应验证数据是否适合其使用情形。

具体而言,客户端应在低权限的“沙盒”进程中,使用比 C/C++ 更安全的编程语言执行任何复杂格式的解析、解释或转换。(如需了解详情,请参阅规则 2)。

对于 ClipboardItemData.text 变体,每个客户端(以及剪贴板服务)中使用的 FIDL 库都会自动执行 UTF-8 验证。

不过,即使是纯粹有效的 UTF-8 文本,一个组件通过剪贴板向另一个组件发送任意文本的能力也可能会带来各种各样的攻击途径,包括:

  • 文本 widget 中的溢出 bug
  • 文本呈现堆栈中的 bug
  • 同形异义字攻击(使用在视觉上相似但实际上是不同字符的字形来欺骗用户,例如,偷偷地将用户发送到钓鱼网域)
  • 应用特有的文本解析中的 bug
  • 意外的代码粘贴到命令提示符中

对于处理或显示任何第三方或用户提供的内容的应用而言,这些问题中的大多数都已成为需要关注的问题,但剪贴板带来了额外的挑战。恶意应用可能会响应有效的用户复制命令,将意外数据(即未明显选择的数据)放置到剪贴板上,从而在用户粘贴数据时诱骗用户充当迷途副手。

未经授权的访问

如上文所述,组件可能希望在未经授权或用户不知情的情况下从剪贴板读取数据或向剪贴板写入数据。

可通过以下方式缓解此问题:

  • 可用于精细地授予或拒绝复制和粘贴功能访问权限的协议
  • 在 fuchsia.ui.clipboard.Focused* 协议中,要求仅向具有输入焦点的前台视图授予剪贴板访问权限

在剪贴板 API 的未来扩展中,我们可能会提供一种用于观察剪贴板 API 事件的协议,系统 shell 可以使用该协议在每次访问剪贴板时显示视觉通知。

未来,随着不受信任的组件和新的剪贴板使用情形的增加,我们将不得不重新考虑未经授权的读取尝试是否应静默返回空的剪贴板项,而不是 ClipboardError.UNAUTHORIZED,以减少有关剪贴板访问的信息泄露。

ViewRef 和焦点验证

剪贴板服务依赖于 Scenic 的焦点链系统来确定当前哪个视图处于聚焦状态,因此有权访问剪贴板。因此,剪贴板对剪贴板焦点位置的确定仅与 Scenic 对输入焦点位置的确定一样可靠,而后者存在一些缺陷:

  • ViewRef(构成焦点链)可以轻松地从一个组件克隆并发送到另一个组件。通过此机制,恶意组件可以协作来模拟彼此,以实现输入焦点。(不过,这至少需要原始所有者信任克隆的 ViewRef 的接收者。)ViewRefViewRef
  • 焦点变化可能会受到竞态条件的影响。

安全审核结果

  • 此 MVP API 受到限制,从而限制了攻击途径。
  • 我们依靠底层 FIDL 反序列化在剪贴板服务收到 UTF-8 字符串时正确验证这些字符串。这是一个攻击面,但我们认为它是安全的,因为该服务的字符串反序列化依赖于 Rust 的 std::str::String 实现,该实现具有内存安全性,并且在 Fuchsia 和更广泛的 Rust 生态系统中都经过了大量测试。
  • 我们认为,ViewRef 解决方案是跟踪视图焦点和用户意图的良好基础,但需注意上述注意事项。

隐私注意事项

需要进行隐私权审核。

未经授权的粘贴

未经授权访问剪贴板内容存在隐私风险,这会使恶意组件能够获取用户放置在键盘上的任何私密数据。如上文所述,fuchsia.ui.clipboard.Focused* 协议通过要求视图至少具有输入焦点(因此在前台可见)才能访问剪贴板来降低此风险。不过,这意味着应用可以在获得短暂焦点后立即获取剪贴板的内容,即使这并非用户的意图。在未来,每当有组件读取剪贴板的内容时,系统 shell 通知都会提醒用户。

旁路攻击

在剪贴板服务的未来迭代中,添加任意数据类型和长度后,内存分析可能会泄露信息(例如剪贴板缓冲区的大小),从而暗示其内容。

剪贴板内容的保留时间

在此阶段,由于仅支持复制短字符串,剪贴板内容将存储在内存中,而不是磁盘上。

剪贴板内容不会通过检查进行记录或公开。

跨安全上下文的访问

Fuchsia 产品可能具有在不同安全环境中运行的界面元素,例如代表不同用户运行,或在预身份验证环境中运行。Fuchsia 必须防止剪贴板内容跨安全上下文边界共享。例如,如果已登录的用户复制了自己的密码,然后锁定了屏幕,则必须无法将该密码粘贴到锁定屏幕对话框中。

这种分离可以通过在每个安全上下文中运行单独的剪贴板服务实例来实现。

测试

此功能将通过单元测试和集成测试进行测试:

  • 剪贴板服务中的单元测试
  • 剪贴板服务的整体集成测试
  • 剪贴板服务、Scenic、输入流水线以及 Flutter 或 Chromium 运行程序之间互动的集成测试。

文档

fuchsia.ui.clipboard API 将通过 fidldoc 进行记录。

我们将通过一个注释详尽的简单组件来演示该协议的使用,该组件用于管理 Scenic 视图(请参阅实现)。

缺点、替代方案和未知因素

在面向用户的操作系统上提供系统级剪贴板服务是无可替代的。仅靠 runner 无法提供此功能,因为它们无法在不同的运行时之间复制和粘贴数据。

在上述设计选择中,另一种方法可能是从限制性更强的“shell 介导的、依赖于焦点”剪贴板协议或完全不受限制的协议(请参阅访问权限级别)开始。

虽然 shell 介导的方法更安全,但对于许多使用情形来说,这种方法可能过于严格,不太实用。例如,它会

  • 防止应用在上下文菜单中提供复制和粘贴命令
  • 干扰基于 Chromium 的运行程序中的 Web 剪贴板 API 的功能

虽然不受限的方法对某些小众应用很有用,但会带来过大的隐私风险,因此不适合作为默认方法提供。

从基于输入焦点的中间访问权限级别开始,我们将能够:

  • 从一开始就优先考虑剪贴板服务中的一些安全和隐私保护保证
  • 在 runner 的 Fuchsia 剪贴板集成中鼓励使用最小权限原则
  • 避免限制过于繁琐,以致无法与现有跑步者进行实际集成

未来的工作

  • 提供用于观察剪贴板 API 事件(读取和写入)的协议,系统 shell 可以使用该协议在访问剪贴板时显示视觉通知。

  • 扩大支持的数据格式和载荷大小范围,尤其是通过使用发送客户端拥有的 VMO。

在先技术和参考资料

紫红色

Fuchsia 之前有一个极简剪贴板 API,以模块化框架代理的形式实现,允许任何具有 fuchsia.modular.Clipboard 能力的组件存储或检索 UTF-8 字符串。(此功能已于 2019 年 11 月被清除。)

Linux:X11

X11 提供多个内容存储区,称为“选择”,其中最常见的是 CLIPBOARD 和 PRIMARY(隐式文本选择剪贴板)。

发送应用会向 X 服务器声明,它“拥有”特定窗口 (XSetSelectionOwner()) 中以给定数据格式表示的这些选择项之一。然后,它会等待进一步的事件。

接收应用在其某个窗口中请求将所选内容转换为其支持的特定格式 (XConvertSelection())。

X 服务器将请求转发给发送应用,如果发送应用支持所请求的格式,则会通过 X 服务器将数据发送给接收应用,以作为响应。如果内容较大,则必须将其分块为不超过 256 KB 的段。

如果源窗口被销毁,选择内容就会丢失,因此在实践中,(1) 大多数应用会将选择内容保存在用户不会关闭的不可见窗口中,并且 (2) 常见的 Linux 发行版包含一个剪贴板管理器,该管理器会获取选择内容的所有权,以便即使原始所有者应用退出,选择内容也能保持有效。

如需了解详情,请访问 https://www.uninformativ.de/blog/postings/2017-04-02/0/POSTING-en.html。

Linux:Wayland

发送应用(必须处于聚焦状态)会通知合成器它具有 wl_data_source,指明该数据源支持哪些 MIME 类型,并注册一个事件监听器。然后等待 send 事件。

接收应用(在尝试粘贴时必须处于聚焦状态)会监听数据 offer 事件,以确定剪贴板是否已填充。当应用想要粘贴时,会调用 wl_data_offer_receive,传入所请求的 MIME 类型和一个文件描述符(通常是管道的写入端)。

发送应用接收到 send 事件并写入给定的文件描述符;接收应用读取另一端。

如需了解详情,请参阅 https://emersion.fr/blog/2020/wayland-clipboard-drag-and-drop/。

Windows (win32)

通过调用 OpenClipboard() 并传入当前窗口的句柄来获取系统剪贴板。发送应用通过调用 EmptyClipboard() 清除所有现有数据,然后调用 SetClipboardData(),传入整数数据类型 ID 和数据本身。发送的数据所用的内存需要使用 GlobalAlloc() 进行分配。

有多种标准剪贴板数据类型;或者,也可以针对自定义全局格式(显然会一直存在,直到重新启动)调用 RegisterClipboardFormat(),或使用特定范围内的 ID 来指示私有剪贴板格式。对于非私有格式,操作系统会获取传入对象的所有权,并负责最终销毁该对象;对于私有格式,当剪贴板被销毁时,原始窗口仍负责清理。对于延迟格式转换,原始窗口可以将 NULL 数据值传递给 SetClipboardData(),然后在响应 WM_RENDERFORMAT 时,呈现所请求的格式,并通过另一次调用 SetClipboardData() 来替换占位符。建议开发者尽可能以多种格式设置剪贴板数据。

接收应用还会检索其窗口的全局剪贴板句柄,检查可用格式的列表(包括发送应用明确放置的格式以及操作系统提供的用于自动转换的格式),调用 GetClipboardData() 以获取特定格式的剪贴板对象的句柄,然后调用 GlobalLock() 以锁定该全球性资源并获取对其内容的访问权限。

还提供了用于注册窗口以监控剪贴板内容更改的方法。

如需了解详情,请参阅 https://docs.microsoft.com/en-us/windows/win32/dataxchg/using-the-clipboard 和 https://docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard-operations。

Android

发送应用会创建一个包含受支持 MIME 类型列表的 ClipData 对象,并使用一个或多个项填充该 ClipData,这些项可以是字符串、指向任何数据的内容 URI 或 Intent(用于应用快捷方式)。发送应用随后会获取对全局 ClipboardManager 对象的引用,并将 ClipData 对象传递给 setPrimaryClip()。

如果复制内容 URI,发送应用必须导出可为该 URI 提供数据的 ContentProvider。

接收应用获取对全局 ClipboardManager 的引用,检查它是否具有主要剪贴对象,然后检查它是否支持任何 ClipData.Item 的数据类型。如果粘贴的是纯字符串,接收应用只需调用 getText()。如果从内容 URI 粘贴,接收应用必须创建一个 ContentResolver 实例,使用给定的 URI 对其进行 query(),然后从返回的 Cursor 中检索数据。

自 Android 12 起,当一个应用访问由另一个应用发送的 ClipData 时,操作系统会显示一条消息框消息。

如需了解详情,请参阅 https://developer.android.com/guide/topics/text/copy-paste。

MacOS

通过 NSPasteboard.general 字段访问系统级“剪贴板”。

发送应用通过将实现 NSPasteboardWriting 协议的对象数组传递给 writeObjects() 方法来复制项。实现者包括字符串和其他常见数据类型,以及 NSPasteboardItem,后者用作自定义数据类型的封装容器。NSPasteboardWriting 提供了一份受支持的统一类型标识符 (UTI,Apple 的 MIME 类型等效项) 列表,以及数据是立即可用还是“承诺”可用。相应地,NSPasteboardItem 可以直接封装数据或数据提供方。

在接收端,应用可以查询其可读取的常规 NSPasteboard 类型,包括可由过滤服务自动转换的类型。然后,它可以选择朗读剪贴板上存储的全部或部分所选内容。

如需了解详情,请参阅 ​​https://developer.apple.com/documentation/appkit/nspasteboard。

iOS

iOS 剪贴板 API 与 macOS 剪贴板 API 类似。通过 UIPasteboard.general 访问系统级剪贴板。

对于发送,有多种方法可用于向剪贴板添加一个或多个带有 UTI 类型标签的项目。还可以插入将延迟提供值的 NSItemProviders。为方便起见,几种标准数据类型在 UIPasteboard 实例上都有自己的可读/可写数组属性:strings、images、urls 和 colors,以及每种类型的单一版本,用于仅访问每种类型的第一个项。

在接收端,可以按索引或按类型检索任意选择的项。

自 iOS 14 起,检索由其他应用放置在粘贴板上的内容会触发系统通知。为了在实际粘贴之前减少虚假通知,iOS 为客户端提供了查询剪贴板上是否存在特定数据类型(hasStrings、hasImages)的功能,而无需访问数据。

如需了解详情,请参阅 https://developer.apple.com/documentation/uikit/uipasteboard。

Web API

虽然网页上的剪贴板互动主要由 Web 浏览器本身处理(受操作系统特性的影响),但也有可用的 JavaScript API,允许网页与剪贴板互动,而不必依赖直接的用户命令。

旧版 ClipboardEvent API 允许脚本监听 DOM Element 上的 "cut"、"copy" 或 "paste" 事件,然后访问事件的 clipboardData 字段,从而按 MIME 类型调用 setData 或 getData。还可以通过编程方式在当前获得焦点的元素上调用 "cut"、"copy" 或 "paste"。出于隐私方面的考虑,在 "cut" 和 "copy" 事件中,无法再以程序化方式粘贴内容,也无法读取剪贴板内容。

现在,我们推出了新的异步 Clipboard API,该 API 受每个网站的用户权限保护。如果用户授予权限,脚本可以访问 navigator.clipboard,然后访问 writeText() 或 readText(),或者访问包含一个或多个以 MIME 类型为键的 blob 的 write() ClipboardItem。(非图片 MIME 类型在某些浏览器中仍处于实验阶段。)

如需了解详情,请参阅 https://whatwebcando.today/clipboard.html 和 https://developer.mozilla.org/en-US/docs/Web/API/Clipboard。

ChromeOS

Chrome 扩展程序可以使用上述剪贴板 API,但需获得相应权限。