近场通信(NFC)使两个设备在近距离内(通常少于几厘米)进行无线通信成为可能。 NFC 是一项国际标准(ISO/IEC 18092),定义了在 13.56 MHz 工作的紧密耦合设备之间用于简单无线互联的接口和协议。

硬件标准在 [[[NFC-STANDARDS]]] 中定义。

本文档定义了基于 NFC 技术启用若干选定用例的 API。本规范当前的范围是 NDEF

低级别 I/O 操作(例如 ISO-DEP、NFC-A/B、NFC-F)以及基于主机的卡模拟(HCE)在当前范围内 受支持。

实现者需要注意,本规范被视为不稳定。 不参与讨论的实现者会发现规范会以不兼容的方式不断变化。希望在规范最终进入候选推荐阶段之前实现本规范的厂商,应订阅 GitHub 上的仓库并参与讨论。

本文档定义了适用于单一产品的符合性标准:实现其所包含接口的 UA(用户代理)。

引言

Web NFC 的用户场景如下:将设备保持在被动供电的 NFC 标签(例如塑料卡或贴纸)近旁,以读取和/或写入数据。

NFC 使用磁感应工作,这意味着读取器(一个有源、有电的设备)会发出微小电荷,从而产生磁场。 该磁场为被动设备供电,被动设备将其转换为电信号以传输数据。因此,当设备处于有效范围内时,会始终进行读取(参见 NFC Analog Specification 和 NFC Digital Protocol,NFC Forum,2006)。 对等连接的工作方式类似,设备会周期性地切换到所谓的发起器模式以扫描目标,然后再切换回目标模式。如果找到目标,则以与标签相同的方式读取数据。

由于 NFC 基于现有的 RFID 标准,许多 NFC 芯片组支持读取 RFID 标签,但其中有些仅由单一厂商支持,并非 NFC 标准的一部分。因此,本文件规定了与 NFC 数据交换格式(NDEF)交互的方法。

术语和约定

使用的扩充巴科斯-诺尔范式(ABNF)表示法在 [[RFC5234]] 中规定。

NFC 表示近场通信,一种在 13.56 MHz 工作的短距离无线技术,使设备在小于 10 cm 的距离内进行通信。NFC 通信协议和数据交换格式基于现有的射频识别(RFID)标准,包括 ISO/IEC 14443 和 FeliCa。 NFC 标准包括 ISO/IEC 18092[5] 以及 NFC Forum 定义的标准。完整列表请参见 NFC Forum 技术规范

NFC adapter 是底层平台中的软件实体,提供对在给定硬件组件(NFC 芯片)中实现的 NFC 功能的访问。设备可能有多个 NFC 适配器,例如内置的一个,或者通过 USB 连接的一个或多个。

NFC tag 是一个被动的 NFC 设备,且未被 blocklisted。当有源 NFC 设备在近旁时,NFC tag 由磁感应供电。支持 NDEFNFC tag 包含单个 NDEF message

读取消息的方式可能通过专有技术发生,这些技术要求读取器和标签来自相同厂商。它们也可能暴露一个 NDEF 消息。

NFC peer 是一个有源、有电的设备,可以与其他设备交互以使用 NFC 交换数据。

如当前规范所述,点对点(peer-to-peer)不被支持。

NFC deviceNFC peerNFC tag

NDEF 是 NFC Forum 数据交换格式(NFC Forum Data Exchange Format)的缩写,一种在 [[!NFC-NDEF]] 中标准化的轻量二进制消息格式。

NDEF message 封装一个或多个应用定义的 NDEF record。NDEF 消息可以存储在 NFC tag 上或在启用 NFC 的设备之间交换。

术语 NFC content 表示发送到或从 NFC tag 接收的所有字节。在当前 API 中,它与 NDEF message 同义。

NFC 标准

NFC 在 NFC Forum 中被标准化,并在 [[NFC-STANDARDS]] 中描述。

NDEF 兼容标签类型

NFC Forum 已规定支持五种不同的标签类型以便与 NFC 设备一起操作。操作系统(如 Android)也要求同样的支持。

除此之外,MIFARE Standard 指定了一种在较旧的 MIFARE Standard 之上使 NDEF 工作的方式,实施者可选择性地支持该方式。

关于 NDEF 映射的说明可以在此找到: MIFARE Classic 作为 NFC 类型 MIFARE Classic 标签

  1. NFC Forum Type 1:该标签基于 ISO/IEC 14443-3A(NFC-A)。标签可重写并可配置为只读。内存大小可在 `96` 字节到 `2` Kbytes 之间。通信速度为 `106` kbit/s。与所有其他类型相比,此类标签没有用于处理多个标签同时处于 NFC 场中的防冲突保护。
  2. NFC Forum Type 2:该标签基于 ISO/IEC 14443-3A(NFC-A)。标签可重写并可配置为只读。内存大小可在 `48` 字节到 `2` Kbytes 之间。通信速度为 `106` kbit/s。
  3. NFC Forum Type 3:该标签基于日本工业标准(JIS)X 6319-4(ISO/IEC 18092),通常称为 FeliCa。标签预配置为可重写或只读。内存为 `2` kbytes。通信速度为 `212` kbit/s 或 `424` kbit/s。
  4. NFC Forum Type 4:该标签基于 ISO/IEC 14443-4 A/B(NFC A、NFC B),因此支持 NFC-A 或 NFC-B 进行通信。此外,标签可以可选地支持 ISO-DEP(ISO/IEC 14443 中定义的数据交换协议(ISO/IEC 14443-4:2008 第4部分:传输协议))。标签预配置为可重写或只读。可变内存,最高可达 `32` kbytes。支持三种不同的通信速度:`106`、`212` 或 `424` kbit/s。
  5. NFC Forum Type 5:该标签基于 ISO/IEC 15693(NFC-V),允许在 ISO/IEC 15693 RF 标签上读取和写入 NDEF 消息,该标签也可被长距离 RFID 读取器访问。NFC 通信限于短距离,可能使用 ISO/IEC 18092 的主动通信模式(Active Communication Mode),在该模式下发送方生成场,以平衡功耗并提高链路稳定性。可变内存,最高可达 `64` kbytes。通信速度为 `26.48` kbit/s。
  6. MIFARE Standard:该标签通常以 MIFARE Classic 或 MIFARE Mini 等品牌出售,基于 ISO/IEC 14443-3A(亦称为 NFC-A,定义于 ISO/IEC 14443-3:2011 第3部分:初始化和防冲突)。标签可重写并可配置为只读。内存大小可在 `320` 到 `4` kbytes 之间。通信速度为 `106` kbit/s。

    MIFARE Standard 不是 NFC Forum 类型,只能被使用 NXP 硬件的设备读取。对基于 MIFARE Standard 的标签进行读写的支持因此不是命名性支持,但由于其在传统系统中的流行性和使用而被包含在内。

除了 NFC Forum 为 NDEF record 标准化的数据类型外,许多商业产品(例如公交卡、门禁卡)可能基于 MIFARE Standard,其依赖特定的 NFC 芯片(卡和读取器来自相同厂商)才能工作。

NDEF 记录和字段

NDEF recordNDEF message 的一部分。每个记录都是包含数据有效载荷及相关类型信息的二进制结构。此外,它还包含有关数据结构的信息,例如有效载荷大小、数据是否在多个记录之间分片等。

通用记录如下所示:

只有前三个字节(图中行)是强制性的。首先是头字节,其次是 TYPE LENGTH fieldPAYLOAD LENGTH field,二者都可以为零。

TNF field(位 `0-2`,类型名格式)指示类型名的格式,通常由本地 NFC 软件栈暴露。该字段可以取二进制值,表示以下 NDEF 记录有效载荷类型:
TNF 值 描述
0 空记录
1 NFC Forum [=well-known type record=]
2 MIME type record
3 Absolute-URL record
4 NFC Forum external type record
5 Unknown record
6 Unchanged record
7 预留用于将来使用

IL field(位 `3`,ID 长度)指示是否存在 ID LENGTH field。如果 IL field 为 `0`,则 ID field 也不存在。

SR field(位 `4`,短记录)指示短记录,其有效载荷长度 <= `255` 字节。普通记录的有效载荷长度可以超过 `255` 字节,最大可达 `4` GB。短记录只使用一个字节指示长度,而普通记录使用 `4` 字节(`2``32``-1` 字节)。

CF field(位 `5`,分片标志)指示有效载荷是否在多个记录之间被 chunked(分片)。

Web NFC 会将所有接收到的分片记录转换为逻辑记录,并在需要时透明地对发送的有效载荷进行分片。

ME field(位 `6`,消息结束)指示该记录是否为 NDEF message 中的最后一个记录。

MB field(位 `7`,消息开始)指示该记录是否为 NDEF message 的第一个记录。

TYPE LENGTH field 是一个无符号 8 位整数,表示 TYPE field 的字节大小。

TYPE field 是一个全局唯一且维护的标识符,用以描述 PAYLOAD field 的类型,其结构、编码和格式由 TNF field 的值决定。

[[[!NFC-RTD]]] 要求 TYPE field 的名称比较必须以不区分大小写的方式进行。

ID LENGTH field 是一个无符号 8 位整数,表示 ID field 的字节大小。

ID field 是以 URI 引用 ([[RFC3986]]) 形式的标识符,具有唯一性,可以是绝对或相对的(在后者情况下,应用必须提供基 URI)。中间和终止的分片记录必须不具有 ID field,其他记录可以具有该字段。

PAYLOAD LENGTH field 表示 PAYLOAD field 的字节大小。如果 SR field 为 `1`,则其大小为 1 字节,否则为 4 字节,分别表示 8 位或 32 位无符号整数。

PAYLOAD field 承载应用字节。NDEF 对数据的任何内部结构保持不透明。注意在后文讨论的某些情况下,该字段可以包含作为数据的 NDEF message

NDEF 记录类型

空 NDEF 记录(TNF 0)

空记录TYPE LENGTH fieldID LENGTH fieldPAYLOAD LENGTH field 必须为 `0`,因此 TYPE fieldID fieldPAYLOAD field 不得存在。

规范类型记录(TNF 1)

NFC Forum 在 [[NFC-RTD]](资源类型定义规范)中标准化了一些有用的子记录类型,称为 规范类型记录,例如文本、URL、媒体等。此外,还有为更复杂交互设计的记录类型,如智能海报(包含用于 url、text、signature 和 actions 的可选嵌入记录)和交接(handover)记录。

存储在 TYPE field规范类型记录 的类型信息可以有两种: 本地类型全局类型

规范本地类型

NFC Forum 的 本地类型 由 NFC Forum 或应用定义,并且总是以小写字母或数字开头。这些通常是简短字符串,仅在包含记录的本地上下文中唯一。当类型含义在包含记录的本地上下文之外不重要且存储使用是硬性约束时使用本地类型。有关如何使用本地类型的示例,请参见 Smart poster

因此,[=local type=] 是根据包含记录类型定义的,因此不需要任何命名空间。基于此,相同的本地类型名称可以在另一个记录类型中以不同含义和不同有效载荷类型重复使用。

规范全局类型

NFC Forum 的 全局类型 由 NFC Forum 定义和管理,通常以大写字母开头。 示例:"`T`" 表示文本,"`U`" 表示 URL,"`Sp`" 表示智能海报,"`Sig`" 表示签名,"`Hc`" 表示交接载体,"`Hr`" 表示交接请求,"`Hs`" 表示交接选择,等等。

文本记录
文本记录 是在 [[NDEF-TEXT]] 规范中定义的 [=规范类型记录=]。 TNF field 为 `1`,TYPE field 为 "`T`"(`0x54`)。 PAYLOAD field 的第一个字节是状态字节,后跟以 US-ASCII 编码的 [=language tag=]。 剩余的有效载荷是实际文本,按状态字节所指示以 UTF-8 或 UTF-16 编码,如下所示:
  • 位 0 到 5 定义 [=language tag=] 的长度。
  • 位 6 为 `0`。
  • 如果位 7 为 `0`,表示有效载荷以 UTF-8 编码,否则若位 7 为 `1` 则以 UTF-16 编码。
URI 记录

URI 记录 在 [[NDEF-URI]] 中定义。 TNF field 为 `1`,TYPE field 为 "`U`"(`0x55`)。 PAYLOAD field 的第一个字节是 URI 标识符代码,实际上是缩写表中的索引,其值会预先加入到 URI 的其余部分。例如,值 `0` 表示不添加前缀,`1` 表示 "`http://www.`",`0x04` 表示 "`https://`" 等等。 有效载荷的其余部分包含作为 UTF-8 字符串的 URI 的其余部分(如果第一个字节为 `0`,则表示整个 URI)。

该 URI 在 [[RFC3987]] 中定义,实际上是一个 UTF-8 编码的 IRI,可以是 URN 或 URL。

智能海报记录
智能海报 在 [[NDEF-SMARTPOSTER]] 中定义,用于将给定的 Web 内容描述为包含一个 NDEF message 作为有效载荷的 NDEF 记录,该消息包含以下记录:
  • 一个单一的强制性 URI record,指向 smart poster 内容。

    [[NDEF-SMARTPOSTER]] 规定,当 NDEF message 中存在 smart poster 记录且该消息中还包含其他 URI records 时,应用程序应该仅使用该 smart poster 记录。

  • 零个或多个 Text records,作为与内容相关的 title record。当存在多个标题记录时,它们必须具有不同的 language tags。应用程序应选择一个 title record 向终端用户展示。
  • 零个或多个 MIME type records,作为与内容相关的 icon recordMIME type 通常为 "`image/jpg`"、"`image/png`"、"`image/gif`" 或甚至 "`video/mpeg`"。应用程序应选择一个 icon record 向终端用户展示。
  • 一个可选的 type record,其具有特定于 smart poster 的 [=local type name=] "`t`",并且 PAYLOAD field 包含指向 URI record 所引用内容的 UTF-8 编码 MIME 类型。
  • 一个可选的 size record,其具有特定于 smart poster 的 [=local type name=] "`s`",并且 PAYLOAD field 包含一个 4 字节的 32 位无符号整数,表示 smart posterURI record 所引用 URL 的对象大小。
  • 一个可选的 action record,其具有特定于 smart poster 的 [=local type name=] "`act`",并且 PAYLOAD field 包含单个字节,其值含义如下:
    描述
    0 执行该动作
    1 稍后保存
    2 打开以供编辑
    3..0xFF 预留用于将来使用
    如果 action record 缺失,则 smart poster 内容没有默认动作。

    在 NDEF 标准化时,值 `0`(“执行该动作”)原本用于诸如发送 SMS、拨打电话或启动浏览器等用例。类似地,值 `1`(“稍后保存”)用于诸如将 SMS 存入收件箱、将 URL 保存到书签或将电话号码保存到联系人等用例。此外,值 `2`(“打开以供编辑”)旨在使用默认应用打开智能海报内容以进行编辑。

    实现不需要对此处定义的动作实现任何标准化行为。在此 API 中,由应用决定其定义的动作(可能包括上述用例)。Web NFC 仅提供这些值。

  • smart poster 还可以包含其他记录,这些记录可以由应用以特定方式处理。
下面的示例显示了一个嵌入了文本和 URL 记录的智能海报记录。
签名记录

NDEF Signature 在 [[NDEF-SIGNATURE]] 中定义。 其 TYPE field 包含 "`Sig`"(`0x53`, `0x69`, `0x67`),其 PAYLOAD field 包含版本、签名和证书链。

如当前规范所述,该项不被支持。

交接(Handover)记录

NFC handover 在 [[NFC-HANDOVER]] 中定义,对应的消息结构允许协商并激活备用通信载体,例如蓝牙或 WiFi。随后协商出的通信载体将被(单独地)用于在两个设备之间执行某些活动,例如向另一设备发送照片、向蓝牙打印机打印或向电视流式传输视频。

如当前规范所述,该项不被支持。

MIME 类型记录(TNF 2)

MIME type record 是存储二进制数据并带有关联 MIME type 的记录。

绝对 URL 记录(TNF 3)

绝对 URL 记录 中,TYPE field 包含 绝对 URL 字符串,而不是有效载荷。

注意:某些平台(如 Windows Phone)在有效载荷中存储了额外的数据,但这些记录中的任何有效载荷数据在其他平台(如 Android)上会被忽略。在 Android 上读取此类记录时,会尝试在 Chrome 中加载该 URL,因此并不打算供客户端应用使用。

外部类型记录(TNF 4)

NFC Forum 的 外部类型记录 用于应用指定的数据类型,并在 [[[NFC-RTD]]] 中定义。

外部类型 是以前缀 `"urn:nfc:ext:"` 开头的 URN,后跟所有者的名称 [=domain=],再添加一个 `U+003A`(`:`),然后是非零的类型名,例如 `"urn:nfc:ext:w3.org:atype"`,在 TYPE field 中存储为 `"w3.org:atype"`。

未知类型记录(TNF 5)

未知记录 是存储不带关联 MIME type 的不透明数据的记录,这意味着可以假定默认的 `application/octet-stream` MIME type。[[NFC-NDEF]] 规范建议 NDEF 解析器将有效载荷存储或转发而不进行处理。

未更改类型记录(TNF 6)

未更改记录 是分片数据集的记录分片,用于除第一条记录之外的任何记录。 分片 的有效载荷分布在多个 NDEF record 中,并遵循以下规则:
  • 初始分片记录设置了 CF field,其 TYPE field 设置为整个分片有效载荷的类型,其 ID field 可选地设置为用于整个分片有效载荷的标识符。其 PAYLOAD LENGTH field 表示本记录中该分片的大小。
  • 中间分片记录设置了 CF field,具有与第一分片相同的 ID field,其 TYPE LENGTH fieldIL field 必须为 `0`,其 TNF field 必须为 `6`(未更改)。
  • 终止分片记录清除了该标志,其余规则与中间分片记录相同。
  • 一个分片有效载荷必须包含在单个 NDEF message 中,因此初始和中间分片记录不能设置 ME field
第一条记录:
中间记录:
最后一条记录:

Web NFC 的任何实现必须将分片记录透明地呈现为单个逻辑记录。

用例

已经在 此处Web NFC 用例 文档中枚举了一些 NFC 用户场景。基本的 Web NFC 交互如下。

读取一个 NFC tag

在使用 Web NFC 的 top-level browsing context 的 {{Document}} 可见时, 读取包含 NDEF messageNFC tag。例如,网页指示用户触碰 NFC 标签,然后从标签接收信息。

NFC tag 写入

用户打开一个可以向 NFC tag 写入的网页。写入操作可能为下列之一:
  1. 向未格式化的 NFC tag 写入。
  2. 向空但已格式化的 NFC tag 写入。
  3. 向已包含 NDEF messageNFC tag 写入。
  4. 向其他可写的 NFC tag 写入(即覆盖通用标签)。

注意,对 NFC tag 的 NFC 写入操作总是同时涉及一次读取操作。

NFC tag 设为只读

用户打开一个可以将 NFC tag 永久设为只读的网页。操作可能为下列之一:
  1. 将未格式化的 NFC tag 永久设为只读。
  2. 将空但已格式化的 NFC tag 永久设为只读。
  3. 将已包含 NDEF messageNFC tag 永久设为只读。

注意,将 NFC tag 永久设为只读总是涉及一次读取操作。

支持多个 NFC 适配器

除了内置适配器外,用户可能为其设备附加一个或多个外部 NFC adapter。用户可以使用任意一个 NFC adapter

功能

Web NFC 规范的高级功能包括以下内容:
  1. 支持具有单个或多个 NFC adapter 的设备。 如果在调用 NFC 功能时存在多个适配器,则 UA 会并行操作所有 NFC adapter
  2. 支持与被动(智能卡、标签等) NFC 设备的通信。
  3. 允许用户对被发现的被动 NFC 设备执行操作(例如读取、写入或收发),并访问在过程中读取到的作为 NDEF message 的有效载荷。
  4. 当兼容设备(例如可写标签)进入范围时,允许用户通过 NDEF record 将有效载荷写入这些设备,作为 NDEF message

示例

本节展示开发者如何使用本规范的各项功能。

功能支持

检测是否支持 Web NFC 可以通过检查 {{NDEFReader}} 对象来完成。注意,这并不保证 NFC 硬件可用。

      if ("NDEFReader" in window) { /* Scan and write NDEF Tags */ }
    

关于写入数据的一般信息

写入数据通常很直接,但在 NFC 写入工作方式上有一些需要注意的地方。

NFC 读取器通过轮询工作,因此为了能够写入标签或将其永久设为只读,需要先找到并读取标签,这意味着需要首先初始化轮询。

如果尚未通过先调用 `scan()` 启动轮询,则 `write()` 和 `makeReadOnly()` 方法会临时启动轮询,直到找到并读取到标签并尝试执行操作为止。

这意味着流程是先在首次找到标签时执行一次读取,然后再执行写入操作。

这也意味着如果 `scan()` 正在运行且你为 `reading` 事件添加了事件监听器,那么在 `write()` 或 `makeReadOnly()` 操作期间该事件监听器会被触发一次,这可能不是预期的行为。

在接下来的各节中,我们将讨论如何轻松处理此行为,但先给出几个简单示例。

写入文本字符串

向 NFC 标签写入文本字符串很简单。

      const ndef = new NDEFReader();
      ndef.write(
        "Hello World"
      ).then(() => {
        console.log("Message written.");
      }).catch(error => {
        console.log(`Write failed :-( try again: ${error}.`);
      });
    

写入 URL

为了写入 URL 类型的 NDEF 记录,只需使用 NDEFMessage。下面示例使用 async/await。

      const ndef = new NDEFReader();
      try {
        await ndef.write({
          records: [{ recordType: "url", data: "https://w3c.github.io/web-nfc/" }]
        });
      } catch {
        console.log("Write failed :-( try again.");
      };
    

在写入时处理初始读取

为了写入,需要找到并读取标签。这使你能够通过检查现有数据或序列号来判断是否确实是要写入的标签。

因此,建议在 `reading` 事件中调用 `write()`。`makeReadOnly()` 也同理。

下例展示如何在通用的 `reading` 处理器和用于单次写入的处理器之间进行协调。

      const ndef = new NDEFReader();
      let ignoreRead = false;

      ndef.onreading = (event) => {
        if (ignoreRead) {
          return; // write pending, ignore read.
        }

        console.log("We read a tag, but not during pending write!");
      };

      function write(data) {
        ignoreRead = true;
        return new Promise((resolve, reject) => {
          ndef.addEventListener("reading", event => {
            // Check if we want to write to this tag, or reject.
            ndef.write(data).then(resolve, reject).finally(() => ignoreRead = false);
          }, { once: true });
        });
      }

      await ndef.scan();
      try {
        await write("Hello World");
        console.log("We wrote to a tag!")
      } catch(err) {
        console.error("Something went wrong", err);
      }
    

带超时的写入调度

有时为写入操作设置时间限制会很有用。比如要求用户触碰标签,如果在一定时间内未找到标签,则超时。

      const ndef = new NDEFReader();
      ndef.onreading = (event) => console.log("We read a tag!");

      function write(data, { timeout } = {}) {
        return new Promise((resolve, reject) => {
          const ctlr = new AbortController();
          ctlr.signal.onabort = () => reject("Time is up, bailing out!");
          setTimeout(() => ctlr.abort(), timeout);

          ndef.addEventListener("reading", event => {
            ndef.write(data, { signal: ctlr.signal }).then(resolve, reject);
          }, { once: true });
        });
      }

      await ndef.scan();
      try {
        // Let's wait for 5 seconds only.
        await write("Hello World", { timeout: 5_000 });
      } catch(err) {
        console.error("Something went wrong", err);
      } finally {
        console.log("We wrote to a tag!");
      }
    

处理扫描错误

本示例展示当 {{NDEFReader/scan}} promise 被拒绝并触发 `readingerror` 时会发生什么。

      const ndef = new NDEFReader();
      ndef.scan().then(() => {
        console.log("Scan started successfully.");
        ndef.onreadingerror = (event) => {
          console.log("Error! Cannot read data from the NFC tag. Try a different one?");
        };
        ndef.onreading = (event) => {
          console.log("NDEF message read.");
        };
      }).catch(error => {
        console.log(`Error! Scan failed to start: ${error}.`);
      });
    

读取单个标签,仅一次

本示例展示如何轻松创建一个便捷函数,该函数只读取单个标签然后停止轮询,从而通过减少不必要的工作来节省电量。

该示例可以很容易扩展为在给定毫秒后超时。

      const ndef = new NDEFReader();

      function read() {
        return new Promise((resolve, reject) => {
          const ctlr = new AbortController();
          ctlr.signal.onabort = reject;
          ndef.addEventListener("reading", event => {
            ctlr.abort();
            resolve(event);
          }, { once: true });
          ndef.scan({ signal: ctlr.signal }).catch(err => reject(err));
        });
      }

      read().then(({ serialNumber }) => {
        console.log(serialNumber);
      });
    

从标签读取数据,并向空标签写入

本示例展示如何读取可存储在标签上的各种不同类型的数据。如果标签未格式化或包含空记录,则写入一条值为 "Hello World" 的文本消息。

      const ndef = new NDEFReader();
      await ndef.scan();
      ndef.onreading = async ({ message }) => {
        if (message.records.length == 0 ||               // unformatted tag
            message.records[0].recordType == "empty") {  // empty record
          await ndef.write({
            records: [{ recordType: "text", data: "Hello World" }]
          });
          return;
        }

        const decoder = new TextDecoder();
        for (const record of message.records) {
          switch (record.recordType) {
            case "text":
              const textDecoder = new TextDecoder(record.encoding);
              console.log(`Text: ${textDecoder.decode(record.data)} (${record.lang})`);
              break;
            case "url":
              console.log(`URL: ${decoder.decode(record.data)}`);
              break;
            case "mime":
              if (record.mediaType === "application/json") {
                console.log(`JSON: ${JSON.parse(decoder.decode(record.data))}`);
              }
              else if (record.mediaType.startsWith("image/")) {
                const blob = new Blob([record.data], { type: record.mediaType });

                const img = document.createElement("img");
                img.src = URL.createObjectURL(blob);
                img.onload = () => window.URL.revokeObjectURL(this.src);

                document.body.appendChild(img);
              }
              else {
                console.log(`Media not handled`);
              }
              break;
            default:
              console.log(`Record not handled`);
          }
        }
      };
    

使用 NFC 标签保存并恢复游戏进度

通过使用自定义记录标识符(在本例中为 "`/my-game-progress`")可以筛选相关数据源。当我们读取数据时,会立即通过使用自定义 NDEF 数据布局写入来更新游戏进度。

      const ndef = new NDEFReader();
      await ndef.scan();
      ndef.onreading = async ({ message }) => {
        if (message.records[0]?.id !== "/my-game-progress")
          return;
        console.log(`Game state: ${ JSON.stringify(message.records) }`);

        const encoder = new TextEncoder();
        const newMessage = {
          records: [{
            id: "/my-game-progress",
            recordType: "mime",
            mediaType: "application/json",
            data: encoder.encode(JSON.stringify({
              level: 3,
              points: 4500,
              lives: 3
            }))
          }]
        };
        await ndef.write(newMessage);
        console.log("Message written");
      };
    

写入与读取 JSON(序列化与反序列化)

使用序列化与反序列化存储和接收 JSON 数据很容易。

      const ndef = new NDEFReader();
      await ndef.scan();
      ndef.onreading = (event) => {
        const decoder = new TextDecoder();
        for (const record of event.message.records) {
          if (record.mediaType === "application/json") {
            const json = JSON.parse(decoder.decode(record.data));
            const article =/^[aeio]/i.test(json.title) ? "an" : "a";
            console.log(`${json.name} is ${article} ${json.title}`);
          }
        }
      };

      const encoder = new TextEncoder();
      await ndef.write({
        records: [
          {
            recordType: "mime",
            mediaType: "application/json",
            data: encoder.encode(JSON.stringify({
              name: "Benny Jensen",
              title: "Banker"
            }))
          },
          {
            recordType: "mime",
            mediaType: "application/json",
            data: encoder.encode(JSON.stringify({
              name: "Zoey Braun",
              title: "Engineer"
            }))
          }]
      });
    

写入数据并打印现有数据

写入数据需要触碰 NFC tag

      const ndef = new NDEFReader();
      await ndef.scan();
      ndef.onreading = async (event) => {
        const decoder = new TextDecoder();
        for (const record of event.message.records) {
          console.log("Record type:  " + record.recordType);
          console.log("MIME type:    " + record.mediaType);
          console.log("=== data ===\n" + decoder.decode(record.data));
        }

        try {
          await ndef.write("Overriding data is fun!");
        } catch(error) {
          console.log(`Write failed :-( try again: ${error}.`);
        }
      };
    

停止监听 NDEF 消息

使用 {{NDEFScanOptions/signal}} 读取 NDEF 消息 3 秒。

      const ndef = new NDEFReader();
      const ctrl = new AbortController();

      await ndef.scan({ signal: ctrl.signal });
      ndef.onreading = () => {
        console.log("NDEF message read.");
      };

      ctrl.signal.onabort = () => {
        console.log("We're done waiting for NDEF messages.");
      };

      // Stop listening to NDEF messages after 3s.
      setTimeout(() => ctrl.abort(), 3_000);
    

写入智能海报消息

      const ndef = new NDEFReader();
      const encoder = new TextEncoder();
      await ndef.write({ records: [
        {
          recordType: "smart-poster",  // Sp
          data: { records: [
            {
              recordType: "url",  // URL record for the Sp content
              data: "https://my.org/content/19911"
            },
            {
              recordType: "text",  // title record for the Sp content
              data: "Funny dance"
            },
            {
              recordType: ":t",  // type record, a local type to Sp
              data: encoder.encode("image/gif") // MIME type of the Sp content
            },
            {
              recordType: ":s",  // size record, a local type to Sp
              data: new Uint32Array([4096]) // byte size of Sp content
            },
            {
              recordType: ":act",  // action record, a local type to Sp
              // do the action, in this case open in the browser
              data: new Uint8Array([0])
            },
            {
              recordType: "mime", // icon record, a MIME type record
              mediaType: "image/png",
              data: await (await fetch("icon1.png")).arrayBuffer()
            },
            {
              recordType: "mime", // another icon record
              mediaType: "image/jpg",
              data: await (await fetch("icon2.jpg")).arrayBuffer()
            }
          ]}
        }
      ]});
    

读取以 NDEF 消息作为有效载荷的外部记录

外部类型记录可以用于创建应用定义的记录。这些记录可以包含一个作为有效载荷的 NDEF message, 其内部可以有自己的 NDEF records,包括在应用上下文中使用的 local types

注意,smart poster 记录类型也包含一个作为有效载荷的 NDEF message

由于 NDEF 不保证记录的顺序,使用包含 NDEF message 作为有效载荷的外部类型记录,对于封装相关数据可能很有用。

本示例展示如何读取用于社交发布的外部记录,该记录包含一个 NDEF message,其中包含一个文本记录和一个具有 local type "act"(动作)的记录, 其定义借用自 smart poster,但在本地应用上下文中使用。

      const ndef = new NDEFReader();
      await ndef.scan();
      ndef.onreading = (event) => {
        const externalRecord = event.message.records.find(
          record => record.type == "example.com:smart-poster"
        );

        let action, text;

        for (const record of externalRecord.toRecords()) {
          if (record.recordType == "text") {
            const decoder = new TextDecoder(record.encoding);
            text = decoder.decode(record.data);
          } else if (record.recordType == ":act") {
            action = record.data.getUint8(0);
          }
        }

        switch (action) {
          case 0: // do the action
            console.log(`Post "${text}" to timeline`);
            break;
          case 1: // save for later
            console.log(`Save "${text}" as a draft`);
            break;
          case 2: // open for editing
            console.log(`Show editable post with "${text}"`);
            break;
        }
      };
    

写入以 NDEF 消息作为有效载荷的外部记录

外部类型记录可以用于创建应用定义的记录,这些记录甚至可以包含一个作为有效载荷的 NDEF message

      const ndef = new NDEFReader();
      await ndef.write({ records: [
        {
          recordType: "example.game:a",
          data: {
            records: [
              {
                recordType: "url",
                data: "https://example.game/42"
              },
              {
                recordType: "text",
                data: "Game context given here"
              },
              {
                recordType: "mime",
                mediaType: "image/png",
                data: getImageBytes(fromURL)
              }
            ]
          }
        }
      ]});
    

在外部记录内写入和读取未知记录

未知类型记录在外部类型记录内可能很有用,因为开发者知道它们代表什么,因此可以避免指定 MIME 类型。

      const encoder = new TextEncoder();
      const ndef = new NDEFReader();
      await ndef.write({ records: [
        {
          recordType: "example.com:shoppingItem", // External record
          data: {
            records: [
              {
                recordType: "unknown", // Shopping item name
                data: encoder.encode("Food")
              },
              {
                recordType: "unknown", // Shopping item description
                data: encoder.encode("Provide nutritional support for an organism.")
              }
            ]
          }
        }
      ]});
    
      const ndef = new NDEFReader();
      await ndef.scan();
      ndef.onreading = (event) => {
        const shoppingItemRecord = event.message.records[0];
        if (shoppingItemRecord?.recordType !== "example.com:shoppingItem")
          return;

        const [nameRecord, descriptionRecord] = shoppingItemRecord.toRecords();

        const decoder = new TextDecoder();
        console.log("Item name: " + decoder.decode(nameRecord.data));
        console.log("Item description: " + decoder.decode(descriptionRecord.data));
      };
    

将 NFC 标签永久设为只读

将 NFC 标签永久设为只读很直接。

      const ndef = new NDEFReader();
      ndef.makeReadOnly().then(() => {
        console.log("NFC tag has been made permanently read-only.");
      }).catch(error => {
        console.log(`Operation failed: ${error}`);
      });
    
      const ndef = new NDEFReader();
      try {
        await ndef.write("Hello world");
        console.log("Message written.");
        await ndef.makeReadOnly();
        console.log("NFC tag has been made permanently read-only after writing to it.");
      } catch (error) {
        console.log(`Operation failed: ${error}`);
      }
    

数据表示

接口 NDEFMessage

任意 NDEF message 的内容由 NDEFMessage 接口暴露:

      [SecureContext, Exposed=Window]
      interface NDEFMessage {
        constructor(NDEFMessageInit messageInit);
        readonly attribute FrozenArray<NDEFRecord> records;
      };

      dictionary NDEFMessageInit {
        required sequence<NDEFRecordInit> records;
      };
    

records 属性表示定义该 NDEF message 的一列 NDEF record

NDEFMessageInit 字典用于初始化一个 NDEF message

接口 NDEFRecord

任意 NDEF record 的内容由 NDEFRecord 接口暴露:

      [SecureContext, Exposed=Window]
      interface NDEFRecord {
        constructor(NDEFRecordInit recordInit);

        readonly attribute USVString recordType;
        readonly attribute USVString? mediaType;
        readonly attribute USVString? id;
        readonly attribute DataView? data;

        readonly attribute USVString? encoding;
        readonly attribute USVString? lang;

        sequence<NDEFRecord>? toRecords();
      };

      dictionary NDEFRecordInit {
        required USVString recordType;
        USVString mediaType;
        USVString id;

        USVString encoding;
        USVString lang;

        any data; // DOMString or BufferSource or NDEFMessageInit
      };
    

mediaType 属性表示 NDEF record 有效载荷的 MIME type

recordType 属性表示 NDEF record 的类型。

id 属性表示 record identifier,它是一个绝对或相对的 URL。标识符的唯一性仅由生成者保证,本规范不负责保证。

NFC NDEF 规范使用术语“message identifier”和“payload identifier”而不是 record identifier,但该标识符与每个记录相关联而非与消息(记录集合)相关联,并且在没有有效载荷时也可能存在。

encoding 属性表示在有效载荷为文本数据时用于对其进行编码的 [=encoding/name|encoding name=]。

lang 属性表示在有效载荷已编码时的 [=language tag=]。

language tag 是一个匹配 [[BCP47]] 规范中 Language-Tag 产生式的 string(关于可能值的权威列表,请参见 IANA Language Subtag Registry)。语言范围由一个或多个由 U+002D HYPHEN-MINUS ("-") 分隔的 subtags 组成。例如,'en-AU' 表示澳大利亚的英语,'fr-CA' 表示加拿大的法语。在不参考 IANA Language Subtag Registry 的情况下,可根据 [[RFC5646]] 第 2.2.9 节验证其结构有效性的语言标签被视为结构上有效。

data 属性表示 PAYLOAD field 的数据。

toRecords() 方法在被调用时,必须返回对给定 NDEF Record 运行 convert NDEFRecord.data bytes 的结果。

NDEFRecordInit 字典用于使用其 record type recordType、可选的 record identifier id 和有效载荷数据 data 来初始化一个 NDEF record

此外,还有仅适用于某些 record types 的可选字段:
  • "mime":可选的 MIME type mediaType
  • "text":可选的 [=encoding/label|encoding label=] encoding 和 [=language tag=] lang

NDEFRecordInit 的数据类型映射到 NDEF record 的规则在处理数据的算法步骤中给出,并在 [[[#steps-receiving]]] 和 [[[#writing-content]]] 节中描述。

要对给定 |record:NDEFRecord| 执行 convert NDEFRecord.data bytes,请运行以下步骤:

  1. 令 |bytes:byte sequence| 为 record 的 data 属性的值。
  2. 令 |recordType:record type| 为 |record| 的 recordType 属性的值。
  3. 如果 |recordType| 的值为 "`smart-poster`",则返回对给定 |bytes| 和 `"smart-poster"` 运行 parse records from bytes 的结果。
  4. 如果对 |recordType| 运行 validate external type 返回 true,则返回对给定 |bytes| 和 `"external"` 运行 parse records from bytes 的结果。
  5. 否则,抛出 {{"NotSupportedError"}}。

record type 字符串

该字符串定义了 NDEFRecord 的允许记录类型。[[[#data-mapping]]] 节描述了它如何映射到 NDEF record 类型。

一个标准化的 well known type name 可以是以下之一:

字符串 "empty"
表示一个 empty NDEFRecord 的值。
字符串 "text"
表示一个 Text record 的值。
字符串 "url"
表示一个 URI record 的值。
字符串 "smart-poster"
表示一个 Smart poster 记录的值。
字符串 "absolute-url"
表示一个 absolute-URL record 的值。
字符串 "mime"
表示一个 MIME type record 的值。
字符串 "unknown"
表示一个 unknown record 的值。

除了 [=well known type names=],组织也可以创建自定义的 external type name,它是由 [=domain=] 名称和自定义类型名组成的字符串,二者以冒号 `U+003A` (`:`) 分隔。

应用也可以使用 local type name,该字符串必须以小写字符或数字开头,表示 NFC Forum [=local type=] 的类型。它通常用于作为父 NDEFRecord 有效载荷的 NDEFMessage 中的记录,例如在 smart poster 中。local type 的上下文是其父记录(其有效载荷为包含该记录的 NDEFMessage),并且 local type name 不应与该上下文中使用的任何其他类型名冲突。

Web NFC 的任何实现必须将分片记录透明地呈现为单个逻辑记录,因此 unchanged record 不会被显式表示。

两个 well-known type records(包括任何 NFC Forum 的 local type 和任何 NFC Forum 的 global type)必须逐字符以区分大小写的方式比较。

两个 external types 必须逐字符以不区分大小写的方式比较。

任何 well-known type recordexternal type 的二进制表示必须作为相对 URI(RFC 3986)写入,分别省略命名空间标识符 (NID) "`nfc`" 与命名空间特定字符串 (NSS) "`wkt`" 和 "`ext`",即省略 "`urn:nfc:wkt:`" 与 "`urn:nfc:ext:`" 前缀。例如,"`urn:nfc:ext:company.com:a" 存储为 "`company.com:a`",而 Text recordwell-known type records 为 "`urn:nfc:wkt:T`",但存储为 "`T`"。

数据映射

NDEFRecordInit 的数据类型到 NDEF record 类型的映射(在 [[[#writing-content]]] 节中使用)如下:

{{recordType}} {{mediaType}} {{data}} record type [=TNF field=] [=TYPE field=]
"`empty`" 未使用 未使用 Empty record 0 未使用
"`text`" 未使用 {{BufferSource}} 或
{{DOMString}}
[=Well-known type record=] 1 "`T`"
"`url`" 未使用 {{DOMString}} [=Well-known type record=] 1 "`U`"
"`smart-poster`" 未使用 {{NDEFMessageInit}} [=Well-known type record=] 1 "`Sp`"
[=local type name=] 前缀为冒号 `U+003A` (`:`),例如 "`:act`"、"`:s`" 和 "`:t`" 未使用 {{BufferSource}} 或 {{NDEFMessageInit}} [=Local type=] record* 1 [=local type name=],例如 "`act`"、"`s`" 和 "`t`"
"`mime`" [= MIME type =] {{BufferSource}} MIME type record 2 [= MIME type =]
"`absolute-url`" 未使用 {{DOMString}} url Absolute-URL record 3 [=Absolute-URL=]
[=external type name=] 未使用 {{BufferSource}} 或
{{NDEFMessageInit}}
External type record 4 [=external type name=]
"`unknown`" 未使用 {{BufferSource}} [=Unknown record=] 5 未使用

* 一个 [=local type=] 记录必须嵌入在另一个记录的 NDEFMessage 有效载荷中。

NDEF record 类型到 NDEFRecord 的映射(用于描述传入的 NDEF message 的 [[[#steps-receiving]]] 节)如下。

record type [=TNF field=] [=TYPE field=] {{recordType}} {{mediaType}}
[=Empty record=] 0 未使用 "`empty`" null
[=Well-known type record=] 1 "`T`" "`text`" null
[=Well-known type record=] 1 "`U`" "`url`" null
[=Well-known type record=] 1 "`Sp`" "`smart-poster`" null
[=Local type=] record* 1 [=local type name=],例如 "`act`"、"`s`" 和 "`t`" [=local type name=] 前缀为冒号 `U+003A` (`:`),例如 "`:act`"、"`:s`" 和 "`:t`" null
[=MIME type record=] 2 [=MIME type=] "`mime`" NDEF 记录中使用的 MIME type
[=Absolute-URL record=] 3 URL "`absolute-url`" null
[=External type record=] 4 [=external type name=] [=external type name=] null
Unknown record 5 未使用 "`unknown`" null

NDEFReader 对象

NDEFReader 是一个将 NFC 功能暴露给browsing context 的对象:当设备(如标签)位于磁感应场内时读取 NDEF messages。此外,它也用于向范围内的 NFC tag 写入 NDEF messages

    typedef (DOMString or BufferSource or NDEFMessageInit) NDEFMessageSource;

    [SecureContext, Exposed=Window]
    interface NDEFReader : EventTarget {
      constructor();

      attribute EventHandler onreading;
      attribute EventHandler onreadingerror;

      Promise<undefined> scan(optional NDEFScanOptions options={});
      Promise<undefined> write(NDEFMessageSource message,
                                     optional NDEFWriteOptions options={});
      Promise<undefined> makeReadOnly(optional NDEFMakeReadOnlyOptions options={});
    };

    [SecureContext, Exposed=Window]
    interface NDEFReadingEvent : Event {
      constructor(DOMString type, NDEFReadingEventInit readingEventInitDict);

      readonly attribute DOMString serialNumber;
      [SameObject] readonly attribute NDEFMessage message;
    };

    dictionary NDEFReadingEventInit : EventInit {
      DOMString? serialNumber = "";
      required NDEFMessageInit message;
    };
  

NDEFMessageSource 是一个联合类型,表示 {{NDEFReader/write()}} 方法所接受的参数类型。

NDEFReadingEvent 是在有新的 NFC 读取时分派的事件。serialNumber 属性表示用于防冲突和标识的设备序列号;若不可用则为空字符串。message 是一个 NDEFMessage 对象。

NDEFReadingEventInit 用于通过成员 message 以序列号和 NDEFMessageInit 数据来初始化新事件。如果未提供或为 null 的 serialNumber,则使用空字符串初始化事件。

虽然大多数标签都有稳定的唯一标识符(UID),但并非所有标签都有,有些标签甚至每次读取都会生成一个随机数。序列号通常由 4 或 7 个数字组成,以 `:` 分隔。

一个 {{NDEFReader}} 对象具有以下 内部槽位

Internal Slot Initial value Description (non-normative)
[[\WriteOptions]] null 写入所用的 {{NDEFWriteOptions}} 值。
[[\WriteMessage]] null 将要写入的 {{NDEFMessage}}。初始未设置。

onreading 是一个 {{EventHandler}},用于在有新读取可用时通知。

onreadingerror 是一个 {{EventHandler}},用于在读取期间发生错误时通知。

与 settings object 关联的 NFC 状态

支持 NFC 的active documentrelevant settings object 关联一个 NFC state 记录,其包含以下内部槽位

Internal Slot Initial value Description (non-normative)
[[\Suspended]] false 一个布尔标志,指示 NFC 功能是否suspended,初始为 false。
[[\ActivatedReaderList]] set 一个 {{NDEFReader}} 实例的set
[[\PendingWrite]] empty 一个 <|promise:Promise|, |writer:NDEFReader|> 元组,其中 |promise| 保存一个挂起的 {{Promise}},|writer| 保存一个 {{NDEFReader}}。
[[\PendingMakeReadOnly]] empty 一个 <|promise:Promise|, |writer:NDEFReader|> 元组,其中 |promise| 保存一个挂起的 {{Promise}},|writer| 保存一个 {{NDEFReader}}。

activated reader objects[[\ActivatedReaderList]] 内部槽位的值。

pending write tuple[[\PendingWrite]] 内部槽位的值。

pending makeReadOnly tuple[[\PendingMakeReadOnly]] 内部槽位的值。

[[\Suspended]] 内部槽位为 true,则表示NFC is suspended

suspend NFC,将 [[\Suspended]] 内部槽位设为 true。

resume NFC,将 [[\Suspended]] 内部槽位设为 false。

内部槽位仅作为本规范中的记法使用,实现不必将其映射为显式的内部属性。

处理 NFC 适配器

实现可以(MAY)根据本规范中描述的算法步骤使用多个 NFC adapter

获取权限

Web NFC API 是一个 [=default powerful feature=],其 [=powerful feature/name=] 为 "nfc"。

obtain permission,执行以下步骤:
  1. 令 |state:PermissionState| 为使用 "nfc" [=getting the current permission state=] 的结果。
  2. 如果 |state| 是 {{PermissionState["granted"]}}(即已通过 [[[PERMISSIONS]]] API 将权限授予该originglobal object),则返回 true。
  3. 否则,如果 |state| 是 {{PermissionState["prompt"]}},则可选择request permission to use "nfc" 向用户请求权限。如获授予,则返回 true。

    request permission 步骤尚未明确定义。此时 UA 会就给定 originglobal object 使用 "nfc" 的策略询问用户;若用户授予权限,则返回 true。

  4. 返回 false。

处理可见性变化

本规范的 [=page visibility change steps=],给定字符串 |visibilityState| 和 {{Document}} |document|,如下:

  1. 若 |visibilityState| 为 `"visible"`,resume NFC 并中止这些步骤。
  2. 否则,执行以下步骤:
    1. Suspend NFC
    2. 尝试abort a pending write operation
    3. 尝试abort a pending make read-only operation

术语 suspended 指 NFC 操作被暂停,这意味着在被暂停期间,任何 NDEFReader 都不会写入 NFC content,且不会向任何 {{NDEFReader}} 呈现接收到的 NFC content

中止挂起的写入操作

要尝试abort a pending write operation,执行以下步骤:
  1. 如果不存在pending write tuple |tuple|,则中止这些步骤。
  2. 如果 |tuple| 的 writer 已经启动了正在进行的 NFC 数据传输,则中止这些步骤。
  3. 使用 {{"AbortError"}} 拒绝 |tuple| 的 promise,并中止这些步骤。

    拒绝该 promise 将清除 pending write tuple

中止挂起的只读化操作

要尝试abort a pending make read-only operation,执行以下步骤:
  1. 如果不存在pending makeReadOnly tuple |tuple|,则中止这些步骤。
  2. 如果 |tuple| 的 writer 已经将某个 NFC 标签永久设为只读,则中止这些步骤。
  3. 使用 {{"AbortError"}} 拒绝 |tuple| 的 promise,并中止这些步骤。

    拒绝该 promise 将清除 pending makeReadOnly tuple

释放 NFC

要在environment settings objectrelease NFC,执行以下步骤:

  1. Suspend NFC
  2. 尝试abort a pending write operation
  3. 尝试abort a pending make read-only operation
  4. 清空activated reader objects
  5. 释放底层平台上的 NFC 资源。

UA 必须在文档的relevant settings object 上作为额外的unloading document cleanup stepsrelease NFC

NDEFWriteOptions 字典

      dictionary NDEFWriteOptions {
        boolean overwrite = true;
        AbortSignal? signal;
      };
    

overwrite 属性的值为 false 时,write 算法 将读取 NFC tag 以确定其上是否已有 NDEF 记录;若有,则不会执行任何挂起的写入。

signal 属性允许中止 {{NDEFReader/write()}} 操作。

NDEFMakeReadOnlyOptions 字典

      dictionary NDEFMakeReadOnlyOptions {
        AbortSignal? signal;
      };
    

signal 属性允许中止 {{NDEFReader/makeReadOnly()}} 操作。

NDEFScanOptions 字典

        dictionary NDEFScanOptions {
          AbortSignal signal;
        };
      

signal 属性允许中止 {{NDEFReader/scan()}} 操作。

Writing content

本节描述如何在定时器到期前,当某个 NFC tag 下次进入近距离范围时,向其写入一个 NDEF message。任一时刻,对于一个 origin 最多只能设置一个待写入的 NDEF message,直至当前消息被发送或写入被中止。

write() 方法

当调用 NDEFReader.write 方法时,必须运行 write a message 算法:
  1. 令 |p:Promise| 为一个新的 {{Promise}} 对象。
  2. 如果当前未在当前激活的 top-level browsing context 中执行,则以 {{"InvalidStateError"}} 拒绝 |p| 并返回 |p|。
  3. 令 |message:NDEFMessageSource| 为第一个参数。
  4. 令 |options:NDEFWriteOptions| 为第二个参数。
  5. 令 |signal:AbortSignal| 为 |options| 中同名字典成员(若存在),否则为 null。
  6. 若 |signal| 已被 [= AbortSignal/aborted =],则以 |signal| 的 [=AbortSignal/abort reason=] 拒绝 |p| 并返回 |p|。
  7. 若 |signal| 非 null,则向 |signal| 添加以下中止步骤
    1. environment settings object 上运行 abort a pending write operation
  8. [=promise/React=] 至 |p|:
    1. 若 |p| 已 settle(履行或拒绝),则清除(若存在的)pending write tuple
  9. 返回 |p| 并in parallel 运行以下步骤:
    1. obtain permission 步骤返回 false,则以 {{"NotAllowedError"}} 拒绝 |p| 并中止这些步骤。
    2. 若不存在底层 NFC Adapter,或无法建立连接,则以 {{"NotSupportedError"}} 拒绝 |p| 并中止这些步骤。
    3. 若 UA 不允许访问底层 NFC Adapter(例如用户偏好),则以 {{"NotReadableError"}} 拒绝 |p| 并中止这些步骤。
    4. 若底层 NFC Adapter 不支持推送数据,则以 {{"NotSupportedError"}} 拒绝 |p| 并中止这些步骤。
    5. 实现可以(MAY)以 {{"NotSupportedError"}} 拒绝 |p| 并中止这些步骤。

      UA 可能在此时中止写入。终止的原因属于实现细节。例如,实现可能无法支持所请求的操作。

    6. 令 |output| 表示将由 UA 创建的 NDEF message,其为用 |message|、`""` 和 `0` 调用 create NDEF message 的结果。若抛出异常,则以该异常拒绝 |p| 并中止这些步骤。
    7. 尝试 abort a pending write operation

      一次写入会替换所有先前配置的写入操作。

    8. 将 `this`.[[\WriteOptions]] 设为 |options|。
    9. 将 `this`.[[\WriteMessage]] 设为 |output|。
    10. pending write tuple 设为 (`this`, |p|)。
    11. 每当有 NFC tag |device| 进入通信范围时,运行 start the NFC write 步骤。

      NFC is suspended,继续等待,直到用户中止 promise 或某个 NFC tag 进入通信范围。

start the NFC write,执行以下步骤:
  1. 令 |p:Promise| 为 pending write tuple 的 promise。
  2. 令 |writer| 为 pending write tuple 的 writer。
  3. 令 |options:NDEFWriteOptions| 为 |writer|.[[\WriteOptions]]
  4. 若近距离范围内的 NFC tag 不支持用于格式化或写入的 NDEF 技术,则以 {{"NotSupportedError"}} 拒绝 |p| 并返回 |p|。
  5. 校验NFC 未被暂停
  6. 若成功,则执行以下步骤:
    1. 若 |device| 为 NFC tag 且 |options| 的 overwrite 为 false,则读取该标签以检查是否已有 NDEF 记录。若有,则以 {{"NotAllowedError"}} 拒绝 |p| 并返回 |p|。
    2. 令 |output:NDEFMessage| 为 |writer|.[[\WriteMessage]]
    3. 使用与 |device| 处于通信范围内的 NFC adapter,以 |output| 作为缓冲区,启动向 |device| 的数据传输。

      若近距离范围内的 NFC tag 未格式化且可进行 NDEF 格式化,则先格式化并将 |output| 作为缓冲区写入。

      多个适配器应由用户依次使用。几乎不太可能在两个或多个不同且已连接的 NFC adapter 上同时轻触。若确实发生,用户可能需要重复轻触,最好一次一个设备。此处的错误表明需要重试,否则用户可能认为操作在所有连接的 NFC adapter 上都成功了。

    4. 若传输失败,以 {{"NetworkError"}} 拒绝 |p| 并中止这些步骤。
    5. 当传输完成时,resolve |p|。

创建 NDEF message

给定 |source:NDEFMessageSource|、|context:string| 和 |recordsDepth:unsigned short|,要create NDEF message,执行以下步骤:
  1. 根据 |source:NDEFMessageSource| 的类型分支:
    {{DOMString}}
    • 令 |textRecord| 为一个 NDEFRecord,其初始化时 |recordType| 设为 "`text`",|data| 设为 |source|。
    • 令 |records| 为列表 « |textRecord| »。
    • 将 |source| 的 records 设为 |records|。
    {{BufferSource}}
    • 令 |mimeRecord| 为一个 NDEFRecord,其初始化时 |recordType| 设为 "`mime`",|data| 设为 |source|,且 |mediaType| 设为 "`application/octet-stream`"。
    • 令 |records| 为列表 « |mimeRecord| »。
    • 将 |source| 的 records 设为 |records|。
    {{NDEFMessageInit}}
    • 若 |source| 的 records [= list/is empty =],则[= exception/throw =] 一个 {{TypeError}}。
    • 将 |recordsDepth| 加一。
    • 若 |recordsDepth| > `32`,则[= exception/throw =] 一个 {{TypeError}}。
    unmatched type
    • [= exception/throw =] 一个 {{TypeError}}。
  2. 令 |output| 表示 UA 按这些步骤创建的 NDEF message
  3. [= list/For each =] |record:NDEFRecordInit| 属于list |source| 的 records,运行如下步骤:
    1. 令 |ndef| 为运行 create NDEF record(给定 |record:NDEFRecordInit|、|context| 和 |recordsDepth|)的结果,或确保底层平台为 |ndef| 提供等效值。若算法抛出异常 |e|,则以 |e| 拒绝 |promise| 并中止这些步骤。
    2. 将 |ndef| 添加到 |output|。
  4. 若运行 check created records(给定 |output| 和 |context|)抛出 |error: Error|,则以 |error| 拒绝 |promise| 并中止这些步骤。
  5. 返回 |output|。

检查已创建记录

给定 |records: NDEFRecord sequence| 和 |context: string|,要check created records,执行以下步骤:
  1. 若 |context| 为 `"smart-poster"` 且 |records| 不包含且仅包含一个 URI record,或其包含多于一个的 type recordsize recordaction record,则[= exception/throw =] 一个 {{TypeError}}。
  2. 若 |context| 为 `"smart-poster"`,将 URI record 移动到 |records| 的开头。

Web NFC 当前允许在 smart poster 中写入 external typelocal type 记录。同时,也允许 empty records。应用可以忽略 smart poster 内的任何额外记录。

图标记录的媒体类型可以被限制为 `"image/"` 或 `"video/"`,但 [[NDEF-SMARTPOSTER]] 规范实际上允许在 smart poster 中使用其他媒体类型记录,这些可以由应用特定方式处理,例如使用其关联的某种 MIME types 的 vCard 名片。

创建 NDEF record

给定 |record:NDEFRecordInit|、|context:string| 和 |recordsDepth:unsigned short|,要create NDEF record,执行以下步骤:
  1. 令 |ndef| 为 UA 将要创建的一个 NDEF record 的表示。
  2. 若 |record| 的 id 不为 undefined:
    • 令 |identifier| 为 |record| 的 id
    • 将 |ndef| 的 IL field 设为 `1`。
    • 将 |ndef| 的 ID LENGTH field 设为 |identifier| 的长度。
    • 将 |ndef| 的 ID field 设为 |identifier|。
  3. 按 |record| 的 recordType 分支,使用 |record|、|ndef|、|context| 和 |recordsDepth| 调用下面指定的算法并返回结果。若抛出异常 |e|,则以 |e| 拒绝 |promise| 并中止这些步骤。
    "`empty`"
    "`text`"
    "`url`"
    "`mime`"
    "`smart-poster`"
    "`absolute-url`"
  4. 若 |record| 的 recordType 以冒号 `U+003A` (`:`) 开头:
    • 若 |context| 为 `""`(即 |record| 不是另一个 NDEF record 的有效载荷),则以 {{TypeError}} 拒绝 |promise| 并中止这些步骤。
    • 若对 |record| 的 recordType 运行 validate local type 步骤返回 false,则以 {{TypeError}} 拒绝 |promise| 并中止这些步骤。
    • 返回运行 map local type to NDEF(给定 |record|、|ndef|、|context| 和 |recordsDepth|)的结果。若抛出异常 |e|,则以 |e| 拒绝 |promise| 并中止这些步骤。
  5. 若对 |record| 的 recordType 运行 validate external type 返回 true,则返回 map external data to NDEF(给定 |record|、|ndef|、|context| 和 |recordsDepth|)的结果。若抛出异常 |e|,则以 |e| 拒绝 |promise| 并中止这些步骤。
  6. 否则,[= exception/throw =] 一个 {{TypeError}} 并中止这些步骤。

校验 external type

[[NFC-RTD]] 规定 external types 必须包含发布组织的 [=domain=] 名、一个冒号 `U+003A` (`:`),以及至少一个字符长的类型名,例如 "`w3.org:member`",全部以 ASCII 字符存储。

[[NFC-RTD]] 也规定了 URN 前缀 “`urn:nfc:ext:`”,但它不会存储在 NDEF record 中,因此 Web NFC 应用在创建 external type records 时不应(SHOULD NOT)指定该 URN 前缀。

[[NFC-RTD]] 要求 external type 名在表示时带有 URN 前缀 “`urn:nfc:ext:`”,例如在读取 NDEF messages 时。然而,由于 external type records 通过 TNF FIELD 被设为 `0x04` 来区分,类型名冲突的风险很低。此外,W3C TAG 也建议在 Web 上避免使用 URN。因此,Web NFC 在读取或写入 NDEF messages 时都不使用该 URN 前缀。

给定 |input:USVString|,要split external type,执行以下步骤:

  1. 若 |input| 中未找到 `U+003A` (`:`),则返回 failure。
  2. 令 |domain| 为 |input| 起始到首次出现 `U+003A` (`:`) 之前的部分。
  3. 令 |type| 为 |input| 首次出现 `U+003A` (`:`) 之后到末尾的部分(若有)。
  4. 返回 |domain| 与 |type| 的二元组。

给定 |input:USVString|,要validate external type,执行以下步骤:

  1. 令 |domain| 与 |type| 为运行 [=split external type=] 的结果;若失败则返回 false。
  2. 若 |domain| 不是一个 [=valid domain string=],返回 false。
  3. 若 |type| 包含非 [=ASCII alphanumeric=] 的 [=code points=],或包含 `U+0024` (`$`)、`U+0027` (`'`)、`U+0028` `LEFT PARENTHESIS` (`(`)、`U+0029` `RIGHT PARENTHESIS` (`)`)、`U+002A` (`*`)、`U+002B` (`+`)、`U+002C` (`,`)、`U+002D` (`-`)、`U+002E` (`.`)、`U+003B` (`;`)、`U+003D` (`=`)、`U+0040` (`@`)、`U+005F` (`_`),返回 false。
  4. 返回 true。

校验 local type

给定 |input:USVString|,要validate local type,执行以下步骤:

  1. 令 |localTypeName| 为 |input| 第一次出现 `U+003A` (`:`) 之后至末尾的部分。
  2. 若 |localTypeName| 不是 {{USVString}} 或其字节长度超过 255,返回 false。
  3. 若 |localTypeName| 未以小写字母或数字开头,返回 false。
  4. 若 |input| 等于其所包含的 NDEF message 中任何 NDEF recordrecord type,返回 false。
  5. 返回 true。

将空记录映射到 NDEF

给定 |record:NDEFRecordInit|、|ndef|,要map empty record to NDEF,执行以下步骤:
  1. 若 |record| 的 mediaType 不为 undefined,[= exception/throw =] 一个 {{TypeError}}。
  2. 若 |record| 的 id 不为 undefined,[= exception/throw =] 一个 {{TypeError}}。
  3. 将 |ndef| 的 TNF field 设为 `0`(empty record)。
  4. 将 |ndef| 的 IL field 设为 `0`。
  5. 将 |ndef| 的 TYPE LENGTH fieldPAYLOAD LENGTH field 设为 `0`,并省略 TYPE fieldPAYLOAD field
  6. 返回 |ndef|。

将字符串映射到 NDEF

给定 |record:NDEFRecordInit| 和 |ndef|,要map text to NDEF,执行以下步骤:

当客户端特别希望以 [=well-known type record=] 写入文本时这很有用。其他选项包括使用 "`mime`" 值并显式指定文本类 MIME type,这有助于更好地区分,例如使用 "`text/xml`" 或 "`text/vcard`"。

  1. 若 |record| 的 mediaType 不为 undefined,[= exception/throw =] 一个 {{TypeError}}。
  2. 若 |record| 的 data 类型不是 {{DOMString}} 或 {{BufferSource}},[= exception/throw =] 一个 {{TypeError}} 并中止这些步骤。
  3. 令 |documentLanguage:string| 为[=document element=] 的 lang 属性。
  4. 若 |documentLanguage| 为空字符串,将其设为 "`en`"。
  5. 令 |language:string| 为 |record| 的 lang(若其 [= map/exists =]),否则设为 |documentLanguage|。
  6. 根据 |record| 的 data 类型分支:
    {{DOMString}}
    1. 若 |record| 的 encoding 既非 undefined 亦非 "`utf-8`",则[= exception/throw =] 一个 {{TypeError}} 并中止这些步骤。
    2. 令 |encoding label:string| 为 "`utf-8`"。
    {{BufferSource}}
    1. 令 |encoding label:string| 为 |record| 的 encoding(若其 [= map/exists =]),否则为 "`utf-8`"。
    2. 若 |encoding label| 不等于 "`utf-8`"、"`utf-16`"、"`utf-16le`" 或 "`utf-16be`",则[= exception/throw =] 一个 {{TypeError}}。
  7. 令 |encoding name| 为从 |encoding label| obtained 的 [=encoding/name|name=]。
  8. 令 |header:byte| 为如下构造的一个 byte
    1. 若 |encoding name| 等于 UTF-8,则将位 `7` 设为 `0`;否则设为 `1`。
    2. 将位 `6` 设为 `0`(保留)。
    3. 令 |languageLength:octet| 为 |language| string 的长度。
    4. 若 |languageLength| 不能存储于 6 位(|languageLength| > 63),则[= exception/throw =] 一个 {{SyntaxError}}。
    5. 将位 `5` 至位 `0` 设为 |languageLength|。
  9. 令 |data:byte sequence| 为空的 [= byte sequence =]。
    1. 将 |data| 的第一个 byte(位置 0)设为 |header|。
    2. 将位置 1(第二个 byte)到位置 |languageLength| 的 |data| 设为 |language|。
    3. 根据 |record| 的 data 类型分支:
      {{DOMString}}
      1. 令 |stream:byte stream| 为对 |record| 的 data 运行 UTF-8 encode 的结果 byte stream
      2. 从 |stream| Read 字节到 |data|(自位置 |languageLength| + 1 起),直到 read 返回 end-of-stream
      {{BufferSource}}
      1. 将 |record| 的 data 中的字节写入 |data|(自位置 |languageLength| + 1 起)。
  10. 将 |length:unsigned long| 设为 |data| 的 [=byte sequence/length=]。
    1. 将 |ndef| 的 TNF field 设为 `1`([=well-known type record=])。
    2. 将 |ndef| 的 TYPE field 设为 "`T`"(`0x54`)。
    3. 将 |ndef| 的 PAYLOAD LENGTH field 设为 |length|。
    4. 若 |length| > `0`,将 |ndef| 的 PAYLOAD field 设为 |data|。
  11. 返回 |ndef|。

将 URL 映射到 NDEF

给定 |record:NDEFRecordInit| 和 |ndef|,要map a URL to NDEF,执行以下步骤:
  1. 若 |record| 的 mediaType 不为 undefined,[= exception/throw =] 一个 {{TypeError}}。
  2. 若 |record| 的 data 不是 {{DOMString}},则[= exception/throw =] 一个 {{TypeError}}。
  3. 令 |url:URL| 为对 |record| 的 data 进行 parsing 的结果。
  4. 若 |url| 为 failure,[= exception/throw =] 一个 {{SyntaxError}}。
  5. 令 |serializedURL:string| 为对 |url| 的 serialization
  6. 将 [[[NFC-STANDARDS]]] 的 URI Record Type Definition 的第 3.2.2 节所定义的 URI 前缀与 |serializedURL| 进行匹配。
  7. 令 |prefixString:string| 为匹配到的前缀,否则为empty string
  8. 令 |prefixByte:byte| 为对应的前缀编号,否则为 `0`。
  9. 令 |shortenedURL:string| 为自 |serializedURL| 起始处移除 |prefixString| 后的 string
  10. 令 |data:byte sequence| 为空的 [= byte sequence =]。
    1. 将 |data| 的第一个 byte 设为 |prefixByte|。
    2. 令 |stream:byte stream| 为对 |shortenedURL| 运行 UTF-8 encode 的结果 byte stream
    3. 从 |stream| Read 字节到 |data|(自位置 1 起),直到 read 返回 end-of-stream
  11. 将 |length:unsigned long| 设为 |data| 的 [=byte sequence/length=]。
  12. 将 |ndef| 的 TNF field 设为 `1`([=well-known type record=])。
  13. 将 |ndef| 的 TYPE field 设为 "`U`"(`0x55`)。
  14. 将 |ndef| 的 PAYLOAD LENGTH field 设为 |length|。
  15. 若 |length| > `0`,将 |ndef| 的 PAYLOAD field 设为 |data|。
  16. 返回 |ndef|。

将二进制数据映射到 NDEF

给定 |record:NDEFRecordInit| 和 |ndef|,要map binary data to NDEF,执行以下步骤:
  1. 若 |record| 的 data 类型不是 {{BufferSource}},[= exception/throw =] 一个 {{TypeError}}。
  2. 令 |mimeType| 为对 |record| 的 mediaType 运行 parse a MIME type 返回的 MIME type
  3. 若 |mimeType| 为 failure,令 |mimeTypeRecord| 为一个新的 MIME type record,其 type 为 "`application`",subtype 为 "`octet-stream`"。
  4. 将 |arrayBuffer| 设为 |record| 的 data
  5. 将 |length:unsigned long| 设为 |arrayBuffer|.[[\ArrayBufferByteLength]]。
  6. 将 |data:byte sequence| 设为 |arrayBuffer|.[[\ArrayBufferData]]。
  7. 将 |ndef| 的 TNF field 设为 `2`(MIME type)。
  8. 将 |ndef| 的 TYPE field 设为以 |mimeType| 为输入 serialize a MIME type 的结果。
  9. 将 |ndef| 的 PAYLOAD LENGTH field 设为 |length|。
  10. 若 |length| > `0`,将 |ndef| 的 PAYLOAD field 设为 |data|。
  11. 返回 |ndef|。

将 external 数据映射到 NDEF

给定 |record:NDEFRecordInit|、|ndef| 和 |recordsDepth:unsigned short|,要map external data to NDEF,执行以下步骤:
  1. 若 |record| 的 mediaType 不为 undefined,[= exception/throw =] 一个 {{TypeError}}。
  2. 令 |domain| 和 |type| 为对 |record| 的 recordType 运行 [=split external type=] 的结果。
  3. 令 |domain| 为对 |domain| 和 true 运行 domain to ASCII 的结果。
  4. 令 |customTypeName| 为 |domain|、"`:`" 与 |type| 连接后的结果。
  5. 若 |customTypeName| 不是 {{USVString}} 或其字节长度超过 255,[= exception/throw =] 一个 {{TypeError}}。
  6. 将 |ndef| 的 TYPE field 设为 |customTypeName|。
  7. 若 |record| 的 data 类型不是 {{BufferSource}} 或 {{NDEFMessageInit}},[= exception/throw =] 一个 {{TypeError}}。
  8. 将 |ndef| 的 TNF field 设为 `4`(external type record)。
  9. 若 |record| 的 data 类型为 {{BufferSource}},
    1. 将 |arrayBuffer| 设为 |record| 的 data
    2. 将 |length:unsigned long| 设为 |arrayBuffer|.[[\ArrayBufferByteLength]]。
    3. 将 |data:byte sequence| 设为 |arrayBuffer|.[[\ArrayBufferData]]。
    4. 将 |ndef| 的 PAYLOAD LENGTH field 设为 |length|。
    5. 若 |length| > `0`,将 |ndef| 的 PAYLOAD field 设为 |data|。
  10. 若 |record| 的 data 类型为 {{NDEFMessageInit}},
    1. 将 |ndef| 的 PAYLOAD field 设为运行 create NDEF message(给定 |record| 的 data、`"external"` 和 |recordsDepth|)的结果。
    2. 将 |ndef| 的 PAYLOAD LENGTH field 设为其 PAYLOAD field 的长度。
  11. 返回 |ndef|。

将 local type 映射到 NDEF

给定 |record:NDEFRecordInit|、|ndef|、|context:string| 和 |recordsDepth:unsigned short|,要map local type to NDEF,执行以下步骤:
  1. 若 |record| 的 mediaType 不为 undefined,[= exception/throw =] 一个 {{TypeError}}。
  2. 若 |record| 的 data 类型不是 {{BufferSource}} 或 {{NDEFMessageInit}},[= exception/throw =] 一个 {{TypeError}}。
  3. 将 |ndef| 的 TNF field 设为 `1`([=well-known type record=])。
  4. 令 |localTypeName| 为 |record| 的 recordType 中第一次出现 `U+003A` (`:`) 之后至末尾的部分。
  5. 将 |ndef| 的 TYPE field 设为 |localTypeName|,表示该local type name
  6. 若 |context| 为 `"smart-poster"`,|localTypeName| 为 "`s`"(`0x73`),且 |record| 的 data 类型不是 {{BufferSource}} 或其字节长度大于 4,[= exception/throw =] 一个 {{TypeError}}。
  7. 若 |context| 为 `"smart-poster"`,|localTypeName| 为 "`act`"(`0x61` `0x63` `0x74`),且 |record| 的 data 类型不是 {{BufferSource}} 或其字节长度不等于 1,[= exception/throw =] 一个 {{TypeError}} 并中止这些步骤。
  8. 若 |record| 的 data 类型为 {{BufferSource}},
    1. 将 |arrayBuffer| 设为 |record| 的 data
    2. 将 |length:unsigned long| 设为 |arrayBuffer|.[[\ArrayBufferByteLength]]。
    3. 将 |data:byte sequence| 设为 |arrayBuffer|.[[\ArrayBufferData]]。
    4. 将 |ndef| 的 PAYLOAD LENGTH field 设为 |length|。
    5. 若 |length| > `0`,将 |ndef| 的 PAYLOAD field 设为 |data|。
  9. 若 |record| 的 data 类型为 {{NDEFMessageInit}},
    1. 将 |ndef| 的 PAYLOAD field 设为运行 create NDEF message(给定 |record| 的 data、`"local"` 和 |recordsDepth|)的结果。
    2. 将 |ndef| 的 PAYLOAD LENGTH field 设为其 PAYLOAD field 的长度。
  10. 返回 |ndef|。

将 smart poster 映射到 NDEF

给定 |record:NDEFRecordInit|、|ndef| 和 |recordsDepth:unsigned short|,要map smart poster to NDEF,执行以下步骤:
  1. 若 |record| 的 mediaType 不为 undefined,[= exception/throw =] 一个 {{TypeError}}。
  2. 若 |record| 的 data 类型不是 {{NDEFMessageInit}},[= exception/throw =] 一个 {{TypeError}}。
  3. 将 |ndef| 的 TNF field 设为 `1`([=well-known type record=])。
  4. 将 |ndef| 的 TYPE field 设为 "`Sp`"(`0x53` `0x70`)。
  5. 将 |ndef| 的 PAYLOAD field 设为运行 create NDEF message(给定 |record| 的 data、`"smart-poster"` 和 |recordsDepth|)的结果。
  6. 将 |ndef| 的 PAYLOAD LENGTH field 设为其 PAYLOAD field 的长度。
  7. 返回 |ndef|。

将 absolute-URL 映射到 NDEF

给定 |record:NDEFRecordInit|、|ndef| 和 |context:string|,要map absolute-URL to NDEF,执行以下步骤:
  1. 若 |context| 为 `"smart-poster"`,[= exception/throw =] 一个 {{TypeError}}。

    [[NDEF-SMARTPOSTER]] 规范只允许在一个 smart poster 中包含一个 URL,且该 URL 必须是单个 URI record

  2. 若 |record| 的 mediaType 不为 undefined,[= exception/throw =] 一个 {{TypeError}}。
  3. 若 |record| 的 data 不是 {{DOMString}},[= exception/throw =] 一个 {{TypeError}}。
  4. 若对 |record| 的 data 进行 parsing 的结果为 failure,[= exception/throw =] 一个 {{SyntaxError}}。
  5. 将 |arrayBuffer| 设为 |record| 的 data
  6. 将 |data:byte sequence| 设为 |arrayBuffer|.[[\ArrayBufferData]]。
  7. 将 |ndef| 的 TNF field 设为 `3`([=absolute-URL record=])。
  8. 将 |ndef| 的 TYPE field 设为 |data|。
  9. 将 |ndef| 的 PAYLOAD LENGTH field 设为 `0`,并省略 PAYLOAD field
  10. 返回 |ndef|。

Making content read-only

本节描述当某个 NFC tag 在近距离范围内时,如何将其永久设为只读。任一时刻,对于一个 origin 最多只能有一个请求,直到该 NFC tag 被永久设为只读或操作被中止。

makeReadOnly() 方法

当调用 NDEFReader.makeReadOnly 方法时,必须运行 make an NFC tag permanently read-only 算法:
  1. 令 |p:Promise| 为一个新的 {{Promise}} 对象。
  2. 如果当前未在当前激活的 top-level browsing context 中执行,则以 {{"InvalidStateError"}} 拒绝 |p| 并返回 |p|。
  3. 令 |options:NDEFMakeReadOnlyOptions| 为第二个参数。
  4. 令 |signal:AbortSignal| 为 |options| 中同名字典成员(若存在),否则为 null。
  5. 若 |signal| 已被 [= AbortSignal/aborted =],则以 |signal| 的 [=AbortSignal/abort reason=] 拒绝 |p| 并返回 |p|。
  6. 若 |signal| 非 null,则向 |signal| 添加以下中止步骤
    1. environment settings object 上运行 abort a pending make read-only operation
  7. [=promise/React=] 至 |p|:
    1. 若 |p| 已 settle(履行或拒绝),则清除(若存在的)pending write tuple
  8. 返回 |p| 并in parallel 运行以下步骤:
    1. obtain permission 步骤返回 false,则以 {{"NotAllowedError"}} 拒绝 |p| 并中止这些步骤。
    2. 若不存在底层 NFC Adapter,或无法建立连接,则以 {{"NotSupportedError"}} 拒绝 |p| 并中止这些步骤。
    3. 若 UA 不允许访问底层 NFC Adapter(例如用户偏好),则以 {{"NotReadableError"}} 拒绝 |p| 并中止这些步骤。
    4. 实现可以(MAY)以 {{"NotSupportedError"}} 拒绝 |p| 并中止这些步骤。

      UA 可能在此时中止。终止的原因属于实现细节。例如,实现可能无法支持请求的操作。

    5. 尝试 abort a pending make read-only operation

      一次只读化操作会替换所有先前配置的只读化操作。

    6. pending makeReadOnly tuple 设为 (`this`, |p|)。
    7. 每当有 NFC tag |device| 进入通信范围时,运行 start the NFC make read-only 步骤。

      NFC is suspended,继续等待,直到用户中止 promise 或某个 NFC tag 进入通信范围。

start the NFC make read-only,执行以下步骤:
  1. 令 |p:Promise| 为 pending makeReadOnly tuple 的 promise。
  2. 若近距离范围内的 NFC tag 不支持用于格式化的 NDEF 技术,则以 {{"NotSupportedError"}} 拒绝 |p| 并返回 |p|。
  3. 校验NFC 未被暂停
  4. 若成功,则执行以下步骤:
    1. 使用与 |device| 处于通信范围内的 NFC adapter,将 |device| 永久设为只读。
    2. 若操作失败,以 {{"NetworkError"}} 拒绝 |p| 并中止这些步骤。
    3. 当操作完成时,resolve |p|。
    4. 此操作不可逆。一旦某个 NFC 标签被设为只读,就无法再写入。

监听内容

要监听 NFC content,客户端必须通过调用 NDEFReader.scan() 激活一个 {{NDEFReader}} 实例。当在其上附加 "`reading`" 事件的监听器时,客户端即可访问 NFC content

若在 activated reader objects 中存在任意 {{NDEFReader}} 实例,则 UA 必须在所有已连接的 NFC 适配器上监听 NDEF message

scan() 方法

传入的 NFC content 会通过 {{NDEFReader}} 实例进行匹配。

当调用 NDEFReader.scan 方法时,UA 必须运行以下 NFC listen algorithm
  1. 令 |p:Promise| 为一个新的 {{Promise}} 对象。
  2. 如果当前未在当前激活的 top-level browsing context 中执行,则以 {{"InvalidStateError"}} 拒绝 |p| 并返回 |p|。
  3. 令 |reader:NDEFReader| 为该 {{NDEFReader}} 实例。
  4. 令 |options| 为第一个参数。
  5. 令 |signal:AbortSignal| 为 |options| 中同名字典成员(若存在),否则为 null。
  6. 若 |signal| 已被 [= AbortSignal/aborted =],则以 |signal| 的 [=AbortSignal/abort reason=] 拒绝 |p| 并返回 |p|。
  7. 若 |signal| 非 null,则向 |signal| 添加以下 clean up the pending scan 步骤
    1. 将 |reader| 自 activated reader objects 中移除。
    2. activated reader objects [= list/is empty =],则请求停止在所有 NFC adapter 上监听 NDEF message
  8. 返回 |p| 并in parallel 运行以下步骤:
    1. obtain permission 步骤返回 false,则以 {{"NotAllowedError"}} 拒绝 |p| 并中止这些步骤。
    2. 若不存在底层 NFC Adapter,或无法建立连接,则以 {{"NotSupportedError"}} 拒绝 |p| 并中止这些步骤。
    3. 若 UA 不允许访问底层 NFC Adapter(例如用户偏好),则以 {{"NotReadableError"}} 拒绝 |p| 并中止这些步骤。
    4. 若 |reader| 已在 activated reader objects 中,则以 {{"InvalidStateError"}} 拒绝 |p| 并中止这些步骤。
    5. 将 |reader| 添加到 activated reader objects
    6. resolve |p|。
    7. 每当 UA 检测到 NFC 技术时,运行 NFC reading algorithm

NFC 读取算法

要接收 NDEF 内容,运行 NFC reading algorithm
  1. NFC is suspended,则中止这些步骤。
  2. 若近距离范围内的 NFC tag 不支持用于读取或格式化的 NDEF 技术,运行以下子步骤:
    1. [= list/For each =] {{NDEFReader}} 实例 |reader:NDEFReader| 属于 activated reader objects,运行以下子步骤:
      1. 在 |reader| 上 Fire an event 名为 "`readingerror`"。
    2. 中止这些步骤。
  3. 令 |serialNumber:serialNumber| 为设备标识符(由一系列数字组成),若不可用则为 null。
  4. 若 |serialNumber| 非 null,将其设为将每个数字以 ASCII hex digit 表示并以 U+003A(`:`)连接的string,顺序不变。
  5. 令 |message:NDEFMessage| 为一个新的 NDEFMessage 对象,且将 |message| 的 records 设为空list
  6. 若近距离范围内的 NFC tag 未格式化但可进行 NDEF 格式化,令 |input| 为 null。否则,令 |input| 为已接收到的 NDEF message 的表示。

    UA 应(SHOULD)将未格式化的 NFC tag 表示为一个不包含 NDEF recordNDEF message,即其 {{NDEFMessage/records}} 属性为一个空数组。

  7. [= list/For each =] 属于 |input| 的 NDEF record,运行以下子步骤:
    1. 令 |ndef| 为当前 NDEF record 的表示,其中 |typeNameField:number| 对应 TNF field,|payload:byte sequence| 对应 PAYLOAD field 数据。
    2. 令 |record:NDEFRecord| 为对 |ndef| 和 `""` 运行 parse an NDEF record 的结果。
    3. 若 |record| 非 null,则将 |record| append 到 |message| 的 records。
  8. 运行 dispatch NFC content 步骤,给定 |serialNumber| 和 |message|。

分发 NFC 内容

给定类型为 serialNumber 的 |serialNumber:serialNumber| 和类型为 NDEFMessage 的 |message:NDEFMessage|,要dispatch NFC content,执行以下步骤:

  1. [= list/For each =] {{NDEFReader}} 实例 |reader:NDEFReader| 属于 activated reader objects
    1. 在 |reader| 上 fire an event 名为 "`reading`",使用 NDEFReadingEvent,其 serialNumber 属性初始化为 |serialNumber|,message 属性初始化为 |message|。

解析内容

从字节解析记录

给定 |bytes:byte sequence| 和 |context: string|,要parse records from bytes,执行以下步骤:
  1. 若 |bytes| 的长度为 `0`,返回 null。
  2. 令 |records| 为空列表。
  3. 当 |bytes| 仍有未读字节时,运行以下子步骤:
    1. 若 |bytes| 的剩余长度小于 `3`,返回 null。
    2. 若以下任一步骤需要读取超出 |bytes| 剩余长度的字节,返回 null。
    3. 令 |ndef| 为当前 NDEF record 的表示。
    4. 令 |header:byte| 为 |bytes| 的下一个字节。
      1. 令 |messageBegin:boolean|(MB field)为 |header| 的最高位(位 7)。
      2. 若这是这些子步骤的首次迭代且 |messageBegin| 为 false,返回 null。
      3. 令 |messageEnd:boolean|(ME field)为 |header| 的位 6。
      4. 由于分片记录不允许作为子记录,忽略位 5(CF field)。

      5. 令 |shortRecord:boolean|(SR field)为 |header| 的位 4。
      6. 令 |hasIdLength:boolean|(IL field)为 |header| 的位 3。
      7. 令 |ndef| 的 |typeNameField:number|(TNF field)为 |header| 的位 2-0 的整数值。
    5. 令 |typeLength:number| 为 |bytes| 下一个字节(TYPE LENGTH field)的整数值。
    6. 若 |shortRecord| 为 true,令 |payloadLength:number| 为 |bytes| 下一个字节(PAYLOAD LENGTH field)的整数值。
    7. 否则,令 |payloadLength| 为 |bytes| 下一个 4 个字节的整数值。
    8. 若 |hasIdLength| 为 true,令 |idLength:number| 为 |bytes| 下一个字节(ID LENGTH field)的整数值;否则令其为 `0`。
    9. 若 |typeLength| > 0,令 |ndef| 的 |type:string| 为对接下来的 |typeLength|(TYPE field)字节运行 UTF-8 decode 的结果;否则令 |type| 为空字符串。
    10. 若 |idLength| > 0,令 |ndef| 的 |id:string| 为对接下来的 |idLength|(ID field)字节运行 UTF-8 decode 的结果;否则令 |ndef| 的 |id| 为空字符串。
    11. 令 |ndef| 的 |payload| 为最后 |payloadLength|(PAYLOAD field)字节的 byte sequence,该值可以为 `0` 字节。
    12. 令 |record:NDEFRecord| 为对 |ndef| 和 |context| 运行 parse an NDEF record 的结果。
    13. 若 |record| 非 null,将其 append 到 |records|。
    14. 若 |messageEnd| 为 true,
      1. 若对 |records| 和 |context| 运行 check parsed records 抛出 |error|,则以 |error| 拒绝 |promise| 并中止这些步骤。
      2. 否则中止这些子步骤(终止循环)。
  4. 返回 |records|。

检查已解析记录

给定 |records: NDEFRecord sequence| 和 |context: string|,要check parsed records,执行以下步骤:
  1. 若 |context| 为 `"smart-poster"` 且 |records| 不包含且仅包含一个 URI record,或其包含多于一个的 type recordsize recordaction record,则[= exception/throw =] 一个 {{TypeError}}。
  2. 否则返回 true。

解析 NDEF 记录

给定 |ndef| 和 |context:string|,要将其解析为 |record:NDEFRecord| 的 parse an NDEF record,执行以下步骤:
  1. 将 |record| 的 id 设为 |ndef| 的 |id:string|。
  2. 将 |record| 的 lang 设为 null。
  3. 将 |record| 的 encoding 设为 null。
  4. 若 |ndef| 的 |typeNameField:number|(TNF field)为 `0`(empty record):
    1. 将 |record| 的 id 设为 null。
    2. 将 |record| 的 recordType 设为 "`empty`"。
    3. 将 |record| 的 mediaType 设为 null。
    4. 将 |record| 的 data 设为 null。
  5. 若 |ndef| 的 |typeNameField| 为 `1`([=well-known type record=]),则
    1. 若 |ndef| 的 |type:string| 为 "`T`"(`0x54`),将 |record| 设为对 |ndef| 运行 parse an NDEF text record 的结果。
    2. 若 |ndef| 的 |type:string| 为 "`U`"(`0x55`),将 |record| 设为对 |ndef| 运行 parse an NDEF URL record 的结果。
    3. 若 |ndef| 的 |type:string| 为 "`Sp`"(`0x53` `0x70`),将 |record| 设为对 |ndef| 运行 parse an NDEF smart-poster record 的结果。
    4. 若 |ndef| 的 |type:string| 为 "`s`"(`0x73`) 且 |context| 等于 `"smart-poster"`,将 |record| 设为对 |ndef| 运行 parse a smart-poster size record 的结果。
    5. 若 |ndef| 的 |type:string| 为 "`t`"(`0x74`) 且 |context| 等于 `"smart-poster"`,将 |record| 设为对 |ndef| 运行 parse a smart-poster type record 的结果。
    6. 若 |ndef| 的 |type:string| 为 "`act`"(`0x61` `0x63` `0x74`) 且 |context| 等于 `"smart-poster"`,将 |record| 设为对 |ndef| 运行 parse a smart-poster action record 的结果。
    7. 若对 |ndef| 的 |type:string| 运行 validate local type 步骤返回 true,
      1. 若 |context| 不是 `"external"` 或 `"smart-poster"`,[= exception/throw =] 一个 {{TypeError}}。
      2. 将 |record| 设为对 |ndef| 运行 parse a local type record 的结果。
    8. 否则 [= exception/throw =] 一个 {{TypeError}}。
  6. 若 |ndef| 的 |typeNameField| 为 `2`(MIME type record),则将 |record| 设为对 |ndef| 运行 parse an NDEF MIME type record 的结果,或确保底层平台为 |record| 对象的属性提供等效的值。
  7. 若 |ndef| 的 |typeNameField| 为 `3`(absolute-URL record),则将 |record| 设为对 |ndef| 运行 parse an NDEF absolute-URL record 的结果。
  8. 若 |ndef| 的 |typeNameField| 为 `4`(external type record),则将 |record| 设为对 |ndef| 运行 parse an NDEF external type record 的结果,或确保底层平台为 |record| 对象的属性提供等效的值。
  9. 若 |ndef| 的 |typeNameField| 为 `5`(unknown record),则将 |record| 设为对 |ndef| 运行 parse an NDEF unknown record 的结果,或确保底层平台为 |record| 对象的属性提供等效的值。
  10. 否则 [= exception/throw =] 一个 {{TypeError}}。

解析 NDEF well-known `T` 记录

给定 |ndefRecord|,要parse an NDEF text record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`text`"。
  2. 将 |record| 的 mediaType 设为 null。
  3. 若 |ndefRecord| 的 PAYLOAD field 不存在,将 |record| 的 data 设为 null 并返回 |record|。
  4. 令 |header:byte| 为 |ndefRecord| 的 PAYLOAD field 的第一个 byte
  5. 令 |languageLength:octet| 为 |header| 的位 `5` 至位 `0` 的值。
  6. 令 |language:string| 为对第二个 byte 至第 |languageLength| + `1` 个字节(含)运行 ASCII decode 的结果。
  7. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD field 其余部分(不含 |header| 与 |languageLength| 数量的字节)的 byte sequence
  8. 将 |record| 的 lang 设为 |language|。
  9. 若 |header| 的位 `7` 为 `0`,将 |record| 的 encoding 设为 "`utf-8`",否则设为 "`utf-16be`"。
  10. 将 |record| 的 data 设为 |buffer|。
  11. 返回 |record|。

解析 NDEF well-known `U` 记录

给定 |ndefRecord|,要parse an NDEF URL record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`url`"。
  2. 将 |record| 的 mediaType 设为 null。
  3. 若 |ndefRecord| 的 PAYLOAD field 不存在,将 |record| 的 data 设为 null 并返回 |record|。
  4. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD fieldbyte sequence
  5. 令 |prefixByte:byte| 为 |buffer| 的第一个 byte 的值。
  6. 若 |prefixByte| 的值匹配 [[[NFC-STANDARDS]]] URI Record Type Definition 第 3.2.2 节 表 3 中的 URL 扩展代码,
    1. 令 |prefixString:string| 为与 |prefixByte| 值对应的 byte sequence 值。
    2. 将 |record| 的 data 设为 |prefixString| 与 |buffer| 连接后的结果。
  7. 否则,若 |prefixByte| 无匹配,则将 |record| 的 data 设为 |buffer|。
  8. 返回 |record|。

解析 NDEF well-known `Sp` 记录

给定 |ndefRecord|,要parse an NDEF smart-poster record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`smart-poster`"。
  2. 将 |record| 的 mediaType 设为 null。
  3. 若 |ndefRecord| 的 PAYLOAD field 不存在,将 |record| 的 data 设为 null 并返回 |record|。
  4. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD fieldbyte sequence
  5. 将 |record| 的 data 设为 |buffer|。
  6. 返回 |record|。

    应用可以对 data 调用 toRecords() 将其解析为 NDEF records,或自行解析。

给定 |ndefRecord|,要parse a smart-poster size record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`:s`"。
  2. 将 |record| 的 mediaType 设为 null。
  3. 若 |ndefRecord| 的 PAYLOAD field 不为恰好 4 字节,[= exception/throw =] 一个 {{TypeError}}。
  4. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD fieldbyte sequence
  5. 将 |record| 的 data 设为 |buffer|。

    应用可以将该值解析为一个 32 位无符号整数,表示 smart-poster 中 URI 记录所引用对象的大小。

  6. 返回 |record|。
给定 |ndefRecord|,要parse a smart-poster type record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`:t`"。
  2. 将 |record| 的 mediaType 设为 null。
  3. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD fieldbyte sequence

    应用可以将该值解析为一个字符串,包含 [[RFC2048]] 媒体类型,表示 smart-poster 中 URI 记录所引用对象的媒体类型。

  4. 将 |record| 的 data 设为 |buffer|。
  5. 返回 |record|。
给定 |ndefRecord|,要parse a smart-poster action record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`:act`"。
  2. 将 |record| 的 mediaType 设为 null。
  3. 若 |ndefRecord| 的 PAYLOAD field 不为恰好 1 字节,[= exception/throw =] 一个 {{TypeError}}。
  4. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD fieldbyte sequence
  5. 将 |record| 的 data 设为 |buffer|。

    应用可以将该值解析为一个 8 位无符号整数,其取值定义见此处

  6. 返回 |record|。

解析 local type 记录

给定 |ndef|,要parse a local type record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`:`"(`U+003A`)与 |ndef| 的 |type:string| 连接后的结果。
  2. 将 |record| 的 mediaType 设为 null。
  3. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD fieldbyte sequence
  4. 将 |record| 的 data 设为 |buffer|。
  5. 返回 |record|。

解析 NDEF MIME type 记录

给定 |ndefRecord|,要parse an NDEF MIME type record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`mime`"。
  2. 将 |record| 的 mediaType 设为以 |mimeType| 为输入 serialize a MIME type 的结果。
  3. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD fieldbyte sequence(若存在),否则为 null。
  4. 将 |record| 的 data 设为 |buffer|。
  5. 返回 |record|。

解析 NDEF absolute-URL 记录

给定 |ndefRecord|,要parse an NDEF absolute-URL record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`absolute-url`"。
  2. 将 |record| 的 mediaType 设为 null。
  3. 令 |buffer:byte sequence| 为 |ndefRecords| 的 TYPE fieldbyte sequence
  4. 将 |record| 的 data 设为 |buffer|。
  5. 返回 |record|。

解析 NDEF external type 记录

给定 |ndefRecord|,要parse an NDEF external type record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 若对记录的 |ndefRecord| 的 TYPE field 运行 [=validate external type=] 返回 false,返回 null。
  2. 令 |domain| 和 |type| 为以 |ndefRecord| 的 TYPE field 的值运行 [=split external type=] 的结果。
  3. 令 |domain| 为运行 Unicode ToUnicode 的结果,参数为 |domain_name|=|domain|、|CheckHyphens|=false、|CheckBidi|=true、|CheckJoiners|=true、|UseSTD3ASCIIRules|=true、|Transitional_Processing|=false。若结果包含任何错误,返回 null。
  4. 将 |record| 的 recordType 设为 |domain|、"`:`" 与 |type| 连接后的结果。
  5. 将 |record| 的 mediaType 设为 null。
  6. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD fieldbyte sequence(若存在),否则为 null。
  7. 将 |record| 的 data 设为 |buffer|。
  8. 返回 |record|。

解析 NDEF unknown type 记录

给定 |ndefRecord|,要parse an NDEF unknown record 将其解析为 |record:NDEFRecord|,执行以下步骤:
  1. 将 |record| 的 recordType 设为 "`unknown`"。
  2. 将 |record| 的 mediaType 设为 null。
  3. 令 |buffer:byte sequence| 为 |ndefRecords| 的 PAYLOAD fieldbyte sequence(若存在),否则为 null。
  4. 将 |record| 的 data 设为 |buffer|。
  5. 返回 |record|。

阻止列表

本规范依赖一个阻止列表文件来限制网站可访问的 NFC 设备集合。

在 |url:URL| 处解析阻止列表的结果是一个历史字节十六进制值的列表,由以下算法生成:

  1. 获取 |url:URL|,并令 |contents:string| 为其主体,按 UTF-8 解码。
  2. 令 |lines:array| 为将 |contents| 按 `"\n"` 分割得到的结果。
  3. 令 |result:list| 为一个空的list
  4. [= list/For each =] |line:string| in |lines|,执行以下子步骤:
    1. 若 |line| 为空,继续处理下一行。
    2. 若 |line| 以 `"#"` 开头,继续处理下一行。
    3. 若 |line| 包含无效的十六进制值,继续处理下一行。
    4. Append |line| 到 |result|。
  5. 返回 |result|。

blocklist 是在 https://github.com/w3c/web-nfc/blob/gh-pages/blocklist.txtparsing the blocklist 的结果。UA 应定期重新获取阻止列表,但获取频率未作规定。

blocklist 的值包含某设备的 historical bytes 十六进制值,则该 NFC device 被视为 blocklisted。在 ISO 14443-4 术语中,historical bytes 是 RATS(Request for Answer To Select)响应的一个子集。

安全与隐私

信任链

实现需要确保当用户授权 Web NFC API 的某个方法时,只执行该操作且无任何副作用。

默认情况下,NDEF 并不提供使内容被信任的机制,除了允许在写入数据后将标签永久设为只读。这甚至可以在工厂设置中完成。

通过此 API 写入的数据不会自动签名或加密,这与现有原生 NFC API 一致。为保护 NDEF 消息的完整性与真实性,NFC 论坛引入了 [[NDEF-SIGNATURE]]。使用 NDEF signature 和密钥管理由应用自行负责。

若要信任通过 NFC 交换的数据的机密性,应用可以使用加密的 NFC content

若要信任通过 NFC 交换的数据的完整性,应用可以使用 NDEF signature,并基于公钥基础设施(PKI)进行密钥管理。

关于 MIME 类型的一般安全注意事项见 [[RFC2048]] 和 [[RFC2046]]。

隐私影响与实现考量

与条形码或二维码的比较是恰当的,因为 NFC 标签是另一种非人可读的数据交换方式,共享它们可能带来意想不到的隐私与安全影响。网站读取二维码时,必须使用打断式 UI(相机)来拍摄图像,使用户清楚该图像(包括二维码)的内容将可供网页使用,从而明确告知用户正在进行扫描。

使用 NFC 扫描标签需要用户将扫描设备(如手机)置于 NFC 标签的近距离范围内——通常为 5-10 厘米(2-4 英寸)。

当未激活 Web NFC 扫描时扫描标签,会触发主机操作系统处理。因此,从 NFC 标签扫描并启动 URL 或应用并非由 Web NFC 自身处理或支持。

此外,Web NFC 的扫描需要由用户交互触发,且当网站不在焦点或设备屏幕熄灭(即设备未解锁)时,扫描会被暂停。这样设计是为了降低偶发扫描的可能性。

Web NFC 还建议实现从用户体验角度(UX)清晰告知:当把扫描设备靠近 NFC 标签时会进行数据扫描——基本上模仿扫描二维码的 UX 流程。

可采用多种方式实现,例如播放提示音,或在扫描可能发生时显示持续的 UI,比如带有随时可取消能力的模态对话框。

实现还可以显示即将上传的数据,推迟数据分享直到用户确认,甚至显示允许用户选择要分享的记录的 UI。

扫描期间的读写

当用户扫描标签时,网页应用可读取标签上的数据;若其不是只读,也可以向标签写入数据。面向私人使用的消费者贴纸(例如创客社区)通常是未上锁的(可读+可写),而商业部署的 NFC 通常是只读的。

旧协议 SNEP(Simple NDEF Exchange Protocol)允许有源设备(如手机)从另一有源设备接收 NDEF 数据,但 Web NFC 不支持它,且在受支持的原生平台上也在被弃用。

新协议 TNEP(Tag NDEF Exchange Protocol)允许扫描设备(如手机)与主动供电的设备(如 IoT 设备)进行双向通信。Web NFC 目前不支持它,且该协议对可接受的输入有限制,IoT 设备必须确保所接受的记录有效。

如果标签包含隐私敏感数据,则该数据会被分享给站点。如果 UX 要求用户在执行前确认数据交换,则可能不会立即分享。

某些情况下,标签/设备包含隐私敏感数据是显而易见的,例如配备 NFC 的会议胸牌和名片。又如配备 NFC 的血糖仪,可能表明你或近亲为糖尿病患者。

其他情况下,这一点可能不明显,但用户可能曾使用应用或网站向标签写入数据,而该数据在用户不知情的情况下编码了用户 ID 等信息,之后任何其他站点都可能读取到。

私密和意外的数据也可能存储在文件中(例如文字处理文档、PDF 或相机图像),并通过文件上传 API 上传。与文件上传相比,Web NFC API 所采用的缓解措施更强,且数据更不可能与个人直接关联。

扫描期间的读写

对标签的扫描还可能暴露用户位置——若网站能够识别标签并知道该标签在现实世界中的位置(例如安装在博物馆内部)。也可能通过某些线索推断位置,例如 FeliCa NFC 标签多用于日本,但 Web NFC 并不会透露所用的标签技术。

这并不会把网络广告与跟踪模型带入现实世界,因为这需要用户的操作,且不能在后台触发;通过良好的 UX 应清楚地表明扫描处于激活状态。

覆盖现有数据

还存在担忧:向 NFC 标签写入可能损坏或“变砖”。由于 NFC 标签被设计为可由多个用户应用读取,NDEF 标签提供了便捷方式可将设备永久设为只读,甚至可在工厂中配置为只读。

NDEF 是用于读写数据的简单交换格式,而非双向通信。NFC 支持多种基于更低层技术的通信格式(因此不像 NDEF 那样被锁定为只读),且这些格式均不受 Web NFC 支持。

使用 NFC 时应告知用户的事项

本节列出用户在使用 NFC 时应当了解的一些事项。建议实现方在执行相关 NFC 操作之前或期间,帮助用户了解这些事实。

读取的数据会与站点共享

当站点有权读取 NFC 内容时,被扫描标签的数据将与该站点共享,类似上传文件和图片。与任何站点一样,是否信任站点以适当、预期的方式处理这些数据取决于用户。

站点可能修改并覆盖未设为只读的标签数据

已部署的 NFC 方案(例如商店中的标签)应始终设为只读,以确保不会因错误或恶意行为而被修改。

私人标签和贴纸往往出厂即未上锁(可写),用户应意识到此类标签在被扫描时可能被覆盖/修改。

读取固定(例如已安装)标签可能暴露读取位置

固定标签可能在数据中编码其 ID 或位置,这意味着读取该标签会将此信息暴露给知道该标签物理位置的站点,从而推断读取发生的位置。这与登录某项服务相结合,可能将你的位置信息分享给该站点。

写入的数据可被其他具有读取权限的应用和站点读取。任何标签上的 NDEF 数据都可被具有相应访问权限的任何应用或网站读取,因此若不希望如此,数据应以安全方式加密,使仅有被授权者能够读取。

读取场内可能同时存在多个标签

NFC 一次只能读取一个标签,但可以检测到多个标签,并可在其中选择一个进行通信。

这类用例包括:钱包中有多张智能卡(基于 NFC),但不想把卡取出。

这主要适用于由外部硬件读取的支付卡和交通卡,因此不是 Web NFC 的用例。对于 Web NFC,当存在多个可用标签时我们不允许读取,以防以下攻击向量。

存在一种攻击向量:有人将另一个恶意 NFC 标签/贴纸覆盖在合法标签上,以加载错误的应用/站点,或向正确的应用/站点注入错误数据。他们可以通过克隆原标签数据并加以修改来实现——要么将 URL 改为加载恶意应用/站点,要么更改数据以向正确的应用/站点注入恶意数据。示例:标签本应带你访问 https://example.com 却被修改为 https://exаmple.com(其中包含西里尔字母 а)——看起来很像真的,你可能会向恶意站点提供敏感数据。

从标签加载网站不在 Web NFC 的范围内,但建议用户代理在存在多个标签时不要自动加载 URL,以避免上述攻击向量。

通过在存在多个标签时禁止读取,Web NFC 能较好地防止向站点注入错误/恶意数据,因为屏蔽现有 NFC 标签相当困难,需要铁氧体屏蔽且较为明显。金属会干扰磁场,使标签不可读。

资产

需要保护的资产包括:
  • NDEF message 作为整体,尤其是其中的 NDEF records(包括载荷与头部),无论在传输中还是在存储中,在被 Web NFC 触发的操作覆盖时,都需要防止数据泄露与数据修改。这也包括对使用 NFC 标签部署的方案的拒绝服务攻击(例如恶意行为者破坏与方案关联的标签)。
  • 可由使用 Web NFC 的 NFC content 创建者或网站直接或间接确定的用户身份或其他隐私敏感属性。这些数据可能被直接使用或泄露给第三方。例如用户位置、设备标识符和用户标识符。
  • 暴露给使用 Web NFC 的网页的用户数据。尽管网页可能通过 Web NFC 以外的方式收集用户数据,但也可能将这些数据嵌入 NDEF 记录并通过 Web NFC 共享。
  • 用户设备的完整性。对 NFC 标签的读取可能导致用户设备被攻陷,进而导致丢失其他 Web NFC 或平台资产。

攻击者模型

已考虑以下攻击者模式:

威胁

NFC 安全简介见此处。Web NFC 的潜在威胁如下。

指纹识别与数据收集

威胁描述
恶意网页在未经用户同意的情况下收集用户数据、身份或其他隐私敏感属性(如位置),并将其暴露给第三方(将其写入 NFC 标签)。
受影响资产
用户数据、用户身份或其他隐私敏感属性
参与者
使用 Web NFC 的恶意网页所有者、恶意标签所有者。
缓解措施与说明
应(SHOULD)使用户能够了解该网页可通过 NFC 分享哪些数据。使用权限与用户提示来访问个人数据,最小化暴露给 NFC 的用户数据。NFC 标签不应(SHOULD NOT)在未经许可的情况下触发用户设备导航至网站,除非该站点已经在前台或被带到前台并已获授权。用户代理应(SHOULD)参考 地理定位 API 中列出的安全与隐私措施。

NFC 标签被修改

威胁描述
NFC 标签在未经用户同意的情况下被修改,甚至通过设为只读而不可逆。这可能使后续攻击成为可能(使用恶意标签),或成为拒绝服务攻击使一个或多个标签不可用。
受影响资产
存储中的 NDEF 消息记录,包括载荷与头部。
参与者
恶意网页创建者、恶意用户。
缓解措施与说明
写入标签与将其设为只读应当要求权限与用户确认。或者,限制某网页可写入的标签,例如仅允许写入可与其 origin 关联的标签。又或者,允许覆盖,因为不应被写入的标签可通过设为只读进行保护。使用 NDEF signature 来检测 NFC 标签的修改。

传输中的 NDEF 记录被修改

威胁描述
在 Web NFC 与 NFC adapter 和用户设备之间传输的 NDEF record 被修改,用以实施多种中间人攻击或拒绝服务(DoS)攻击。同时,NDEF signature 记录也可能被移除或替换,并配合修改的内容。
受影响资产
传输中的 NDEF record
参与者
恶意中间人用户。
缓解措施与说明
此威胁超出 Web NFC 实现的范围。应用可以使用 NDEF signature 与适当的工具(签名算法、证书、安全策略)来保护 NFC content。此外,加固平台栈。

NDEF 记录载荷泄露

威胁描述
存储在 NFC 标签上的或在 Web NFC 与 NFC adapter 之间传输的 NDEF record 机密载荷被未授权方读取。
受影响资产
传输与存储中的机密 NDEF 消息载荷。
参与者
恶意中间人用户、恶意网页创建者。
缓解措施与说明
为确保机密性,应使用载荷加密与安全通信来进行数据交换、认证与授权(在 Web NFC 与 NFC adapter 之间)。

通过恶意 NFC 标签的主动攻击

威胁描述
恶意标签可能被设备非自愿或自愿地读取,读取的数据可能构成对用户代理的攻击向量。例如,它可能尝试在设备上触发某个操作(这可能有风险),如打开恶意网站,或打开为攻击设备而准备的图片。
受影响资产
用户设备的完整性及所有其他 Web NFC 资产。
参与者
恶意标签创建者。
缓解措施与说明
这是所有现有 NFC 标签的通用问题。数据被视为应用特定。实现需要进行安全加固。由于近距离与读取需特定角度,且需焦点要求,非自愿触碰的概率很低。不应允许针对智能海报和其他标签的自动操作。必须让用户知情,并赋予其控制 NFC 通信期间发生事件的能力。例如,从 smart poster 打开内容、通过 NFC handover 自动连接(可能是恶意的)WiFi 等。不允许来自不受信任的 NFC 标签的操作;可通过 NDEF signature 检查建立信任。

实现的安全机制

Obtaining permission

实现应(SHOULD)使用一种机制来 obtain permission,例如由用户显式授予的权限。建议 UA 使用 [[[PERMISSIONS]]] API 来实现与 NFC 相关的权限。

实现可(MAY)使用按会话/临时权限。

Warning user during NFC operations

实现可(MAY)在网页正在访问 NFC 适配器(例如存在进行中的扫描)时显示覆盖式对话框,以提醒用户。

应用的安全机制

加密 NFC content

为信任通过 NFC 交换的数据的机密性,应用可以使用基于公钥基础设施(PKI)进行密钥管理的加密 NFC content。密钥管理不在 Web NFC 的范围内。

为 NDEF 记录签名

为信任通过 NFC 交换的数据的完整性,用户代理可(MAY)使用基于公钥基础设施的 NDEF signature 进行密钥管理。

对于使用 NDEF signature 1.0 版本([[NFC-SECURITY]])签名的标签,其签名仅覆盖 TYPE fieldID fieldPAYLOAD field,未覆盖 NDEF 头部的第一个字节,从而留下攻击面。[[NFC-SECURITY]] 的 2.0 版本将标签硬件属性纳入签名并允许使用更短的证书。

一个 NDEF signature 覆盖其之前的记录,直到遇到另一个 NDEF signature 或消息起始(NDEF message 的开头)。

为缓解 已知漏洞,建议应用始终使用单个 NDEF signature 为整个 NDEF message 签名,并使用正确的工具链与安全策略来创建与验证签名。

安全策略

本节列出实现的规范性安全策略。

安全上下文

只有secure contexts才允许访问 NFC content。浏览器可(MAY)仅为开发目的而忽略此规则。

可见文档

Web NFC 功能仅允许用于 top-level browsing context 的 {{Document}},且其 {{Document/visibilityState}} 为 `"visible"`。

这也意味着,当显示关闭或设备锁定时,UA 应阻止对 NFC 射频的访问。对于在后台的网页,接收与写入 NFC content 必须被suspended

权限控制

NFC tag 永久设为只读必须(MUST)obtain permission,否则失败。见 [[[#making-content-read-only]]] 一节。

设置用于读取 NFC content 的监听器应当(SHOULD)obtain permission

NFC content 写入 NFC tag 必须(MUST)obtain permission。见 [[[#writing-content]]] 一节。

所有超出当前浏览会话而被保留的权限必须(MUST)可被撤销。

阻止列表

Web NFC 包含一个 blocklist,用于阻止存在漏洞的 NFC 设备被网站利用。

警示物理位置泄露风险

在监听与写入 NFC content 时,UA 可(MAY)警示用户:给定的 origin 可能能够推断物理位置。

限制自动处理

NFC content 的载荷数据不受信任时,UA 不得(MUST NOT)使用其自动处理内容,例如自动打开 NFC tag 中发现的 URL 所指向的网页、安装应用或执行其他操作,除非用户批准。

NFC content 进行签名

建议应用实现以下策略:

致谢

编辑谨此感谢 Jeffrey Yasskin、Anne van Kesteren、Anssi Kostiainen、Domenic Denicola、Daniel Ehrenberg、Jonas Sicking、Don Coleman、Salvatore Iovene、Rijubrata Bhaumik、Wanming Lin、Han Leon、Ryan Sleevi、Balázs Engedy、Theodore Olsauskas-Warren、Reilly Grant、Diego González 和 Daniel Appelquist 对本文档的贡献。

特别感谢 Luc Yriarte 和 Samuel Ortiz 在将 NFC 暴露给 Web 平台方面的初期work,以及他们对当前方法的支持。同时,特别感谢 Elena Reshetova 对“安全与隐私”章节的贡献。