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

RFC-0179:基本剪贴板服务
状态已接受
领域
  • 人机交互
说明

一项基本剪贴板服务的提案,可让用户在组件之间安全地复制和粘贴文本内容,而不受运行程序限制。

问题
  • 97874
Gerrit 更改
  • 672676
作者
  • kpozin@google.com
审核人
提交日期(年-月-日)2022-05-16
审核日期(年-月-日)2020-07-18

总结

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

设计初衷

许多具有图形 shell 的面向用户的现代操作系统都提供剪贴板功能(请参阅前沿技术),让用户能够以交互方式将数据复制到系统提供的内存缓冲区或其他通道,然后再将数据粘贴到其他位置。

过去,Fucsia 以模块化代理的形式实现了基本剪贴板协议,但此代码已于 2019 年移除。

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

许多现有操作系统的剪贴板工具最初设计时没有提供安全配置,允许任何进程随时观察和/或修改剪贴板,而无需用户知情或意图。在 Fuchsia 上,我们的设计宗旨是确保安全性,具体方法如下:

  • 通过精细功能保护剪贴板访问,遵循最小权限原则
  • 尝试限制剪贴板访问,具体取决于输入焦点位于前台窗口中(Fucsia 的 Views 术语中的“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

已咨询

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

社交

  • 在 Fuchsia Input Team 文档审核中讨论
  • 在 Fuchsia 安全工作时间进行讨论

设计

访问权限级别

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

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

在本 RFC 中,我们仅涵盖 (2) 与焦点相关的范围。

范围 (1) 和 (3) 的设计和实现目前尚未计划;它们需要另一个 RFC。

用例

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

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

协议和服务

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

被授予 FocusedWriterRegistryFocusedReaderRegistry 功能的客户端组件将能够分别请求 fuchsia.ui.clipboard.Writerfuchsia.ui.clipboard.Reader 的实例。它们可以随时请求这些连接(假设它们具有有效的 ViewRef),但如果客户端的视图没有输入焦点,WriterReader 的方法将返回错误。

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. 实现在会话领域中运行的一个公开 fuchsia.ui.clipboard.FocusedWriterRegistryfuchsia.ui.clipboard.FocusedReaderRegistry 协议的新剪贴板服务器组件。
  3. 使用一个管理景观视图的简单组件演示与新协议的集成。
  4. 将对新协议的支持集成到 Chromium 和 Flutter 运行程序中。

性能

添加新服务会使用额外的存储空间来存储二进制文件,以及二进制文件和剪贴板内容的内存。每个已注册剪贴板访问权限的客户端都会通过保持打开的 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 和焦点验证

剪贴板服务依靠 Insights 的焦点链系统来确定哪个视图当前获得焦点,因而有权访问剪贴板。因此,剪贴板确定剪贴板焦点的可靠性仅与 smooth 确定输入焦点一样可靠,但这存在一些缺陷:

  • 您可以轻松地克隆构成焦点链的 ViewRef 并将其从一个组件发送到另一个组件。通过这种机制,恶意组件可以协同冒充彼此,以聚焦于输入。(不过这要求 ViewRef 的原始所有者至少信任克隆 ViewRef 的接收方。)
  • 焦点更改受竞态条件的约束。

安全审核结果

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

隐私注意事项

需要进行隐私权审核。

未经授权的粘贴

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

边信道攻击

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

保留剪贴板内容

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

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

跨安全上下文进行访问

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

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

测试

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

  • 剪贴板服务中的单元测试
  • 对剪贴板服务作为一个整体的集成测试
  • 对剪贴板服务、Scape、输入流水线和 Flutter 或 Chromium 运行程序之间的交互的集成测试。

文档

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

该协议的用法将通过一个带充分注释的简单组件来说明,该组件用于管理 View 视图(请参阅实现)。

缺点、替代方案和未知情况

除了在面向用户的操作系统上提供系统级剪贴板服务之外,没有其他可行的替代方案。此功能无法单独由运行程序提供,因为它们无法跨不同运行时复制和粘贴数据。

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

由 shell 中介的方法虽然更安全,但限制性过强,对许多使用场景而言并不实用。例如,

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

不受限制的方法虽然对某些小众应用很实用,但会导致太多的隐私风险,无法作为默认方法提供。

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

  • 从一开始就优先考虑剪贴板服务中的一些安全和隐私保证
  • 在运行程序集成的 Fuchsia 剪贴板中鼓励最小权限原则
  • 避免对于与现有运行程序的实际集成过于繁琐的限制

后续工作

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

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

早期技术和参考资料

紫红

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

Linux:X11

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

发送方应用向 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 粘贴,接收应用必须使用给定的 URI 创建 ContentResolver 实例,query()然后从返回的 Cursor 中检索数据。

从 Android 12 开始,当一个应用访问另一个应用发送的 ClipData 时,操作系统会显示消息框消息。

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

MacOS

您可以通过 NSPasteboard.general 字段访问系统级“粘贴板”。

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

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

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

iOS

iOS Clips API 与 MacOS 类似。系统级粘贴板通过 UIPasteboard.general 进行访问。

可通过多种方法将一个或多个带有 UTI 类型的标签添加到粘贴板中。此外,还可以插入 NSItemProviders 来延迟提供值。为方便起见,在 UIPasteboard 实例上为数种标准数据类型提供了自己的可读取/可写数组属性:stringsimagesurlscolors;还给出了每种数据类型的单个版本,用于只访问每种类型的第一个

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

从 iOS 14 开始,检索由其他应用放置的粘贴板内容会触发系统通知。为了在实际粘贴之前减少虚假通知,iOS 提供了以下功能:无需访问数据,即可查询粘贴板上是否存在某些数据类型(hasStringshasImages)。

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

网络 API

虽然网页上的剪贴板交互主要由网络浏览器本身处理(具体取决于各操作系统的特性),但也有一些 JavaScript API 可用,它们可以让网页与剪贴板进行交互,而无需依赖于用户的直接命令。

旧版 ClipboardEvent API 允许脚本监听 DOM Element 上的 "cut""copy""paste" 事件,然后访问事件的 clipboardData 字段,该字段允许按 MIME 类型调用 setDatagetData。您还可以通过编程方式对当前聚焦的元素调用 "cut""copy""paste"。出于隐私保护方面的考虑,无法再以程序化方式粘贴,而对于 "cut""copy" 事件,剪贴板中的内容将无法读取。

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

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

ChromeOS

Chrome 扩展程序可以使用上述 Clipboard API,具体取决于相关权限。