RFC-0179:基本剪贴板服务 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 提出了基本剪贴板服务的提案,让用户能够在组件之间安全地复制和粘贴文本内容,无论运行器如何。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2022-05-16 |
审核日期(年-月-日) | 2020-07-18 |
摘要
此 RFC 引入了两个框架提供的新协议(fuchsia.ui.clipboard.Writer
和 fuchsia.ui.clipboard.Reader
)以及将实现这些协议的服务,以便用户对文本内容执行复制和粘贴操作。
设计初衷
许多采用图形界面 Shell 的面向用户的现代操作系统都提供剪贴板功能(请参阅先前技术),允许用户以交互方式将数据复制到系统提供的内存缓冲区或其他渠道,并稍后将这些数据粘贴到其他位置。
过去,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 环境中对剪贴板的访问范围可分为三级:
- 由 Shell 中介且取决于焦点
组件仅在响应由图形 Shell 确定的显式用户操作时才能访问剪贴板,并且只有在组件当前具有输入焦点时才能访问剪贴板。 - 依赖于焦点
组件在拥有输入焦点时可以随时访问剪贴板。 - 不受限制
组件可以随时访问剪贴板。
在本 RFC 中,我们仅介绍 (2) 焦点依赖作用域。
目前,我们不打算设计和实现范围 (1) 和 (3);它们需要另一个 RFC。
用例
对于初始 RFC,我们考虑了一些简单但常见的用例:
- 在网络浏览器中,将网页正文中的网址复制到地址栏中
- 将 shell 命令从网络浏览器复制到终端
- 将信息从网络浏览器复制到工作站产品的反馈对话框(该对话框是使用 Flutter 实现的)
协议和服务
我们在合作伙伴 SDK 中引入了两个新的可检测 FIDL 协议:fuchsia.ui.clipboard.FocusedReaderRegistry
和 fuchsia.ui.clipboard.FocusedWriterRegistry
。这些协议将由在会话领域中运行的新组件 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:{file}/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;
};
实现
该过程将分为几个阶段:
- 提交新的
fuchsia.ui.clipboard
FIDL 库(如上文预览所示),以供 API 审核。 - 实现在会话领域中运行的新剪贴板服务器组件,该组件会公开
fuchsia.ui.clipboard.FocusedWriterRegistry
和fuchsia.ui.clipboard.FocusedReaderRegistry
协议。 - 使用用于管理 Scenic 视图的简单组件演示与新协议的集成。
- 将对新协议的支持集成到 Chromium 和 Flutter 运行程序中。
性能
添加新服务会为二进制文件使用额外的存储空间,以及为二进制文件和剪贴板内容使用额外的内存。每个注册剪贴板访问权限的客户端都会通过保持 Zircon 通道处于打开状态而消耗资源。
安全注意事项
需要进行安全审核。
跨组件通信
引入剪贴板服务构成了新的跨组件通信渠道。这为组件有意或无意中利用彼此的漏洞提供了新的可能性。
不可信的内容
剪贴板服务不保证 ClipboardItem
数据或 MIME 类型提示的可信性。因此,客户不应信任所收到的数据,而应验证数据是否适合其用例。
特别要注意,客户端应在权限较低的“沙盒化”进程中使用比 C/C++ 更安全的编程语言执行对复杂格式的任何解析、解释或转换操作。有关详情,请参阅第 2 条规则。
对于 ClipboardItemData.text
变体,每个客户端(以及剪贴板服务)中使用的 FIDL 库会自动执行 UTF-8 验证。
不过,即使是纯正的有效 UTF-8 文本,一个组件能够通过剪贴板向另一个组件发送任意文本,也可能会导致各种利用途径,包括:
- 文本微件中的溢出 bug
- 文本渲染堆栈中的 bug
- 同形异义攻击(使用视觉上相似但实际上是不同字符的字形欺骗用户,例如偷偷将用户引导至钓鱼式攻击网域)
- 应用专用文本解析中的 bug
- 将意外代码粘贴到命令提示中
处理或显示任何第三方或用户提供的内容的应用已经在关注其中的大多数问题,但剪贴板还会带来额外的挑战。恶意应用可能会在响应用户的有效复制命令时,将意外数据(未被显式选择的数据)放置到剪贴板上,诱骗用户在粘贴时充当困惑的代理。
未经授权的访问
如上文所述,组件可能希望在未经授权或用户不知情的情况下读取或写入剪贴板。
可通过以下方式缓解此问题:
- 可用于精细授予或拒绝对复制和粘贴功能的访问权限的协议
- 在
fuchsia.ui.clipboard.Focused*
协议中,要求仅向具有输入焦点的前台视图授予剪贴板访问权限
在剪贴板 API 的未来扩展中,我们可能会提供一种用于监控剪贴板 API 事件的协议,系统 shell 可以使用该协议在每次访问剪贴板时显示视觉通知。
未来,随着不可信组件和新的剪贴板用例的增加,我们将不得不重新考虑,是否应在未经授权的读取尝试时静默返回空的剪贴板项(而非 ClipboardError.UNAUTHORIZED
),以减少有关剪贴板访问权限的信息泄露。
ViewRef
和焦点验证
剪贴板服务依赖于 Scenic 的焦点链系统来确定当前哪个视图具有焦点,因此有权访问剪贴板。因此,剪贴板对剪贴板焦点的确定仅与 Scenic 对输入焦点的确定一样可靠,后者存在一些缺陷:
- 构成焦点链的
ViewRef
可以轻松克隆,并从一个组件发送到另一个组件。通过此机制,恶意组件可以协作冒充对方,以获得输入焦点。(不过,这至少需要ViewRef
的原始所有者信任克隆ViewRef
的收件人。) - 焦点变化可能会受到竞态条件的影响。
安全审核结果
- 此 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 视图,以此来说明该协议的使用方式(请参阅实现)。
缺点、替代方案和未知情况
在面向用户的操作系统上提供系统级剪贴板服务没有可行的替代方案。运行程序无法单独提供此功能,因为它们无法在不同的运行时之间复制和粘贴数据。
在所述的设计选项中,另一种方法可能是从限制性更高的“shell 中介、依赖于焦点”剪贴板协议或完全不受限制的协议(请参阅访问权限级别)开始。
虽然通过 shell 中介的方法更安全,但限制过多,在许多用例中都不可行。例如,它会
- 阻止应用在上下文菜单中提供复制和粘贴命令
- 干扰基于 Chromium 的运行程序中的 Web 剪贴板 API 的功能
虽然无限制方法对某些小众应用很有用,但会带来过多的隐私风险,因此无法作为默认方法提供。
通过先从基于输入焦点的中间访问权限级别入手,我们将能够:
- 从一开始就优先考虑在剪贴板服务中提供一些安全和隐私保障
- 鼓励在 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
发送应用(必须处于聚焦状态)会通知 compositor 它具有 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/zh-cn/windows/win32/dataxchg/using-the-clipboard 和 https://docs.microsoft.com/zh-cn/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/zh-CN/docs/Web/API/Clipboard。
ChromeOS
Chrome 扩展程序可以使用上述剪贴板 API,但需要有相应权限。