1. 谷歌查找我的设备配件(Google Find My Device Accessory)介绍
谷歌查找我的设备配件是与谷歌 “查找我的设备” 应用程序配合使用的配件,旨在帮助用户更方便地追踪和定位个人物品。为了更好的理解它的工作原理,我们先来了解一下谷歌查找我的设备网络FMDN(Google find my device network)。FMDN由四部分组成:附件(Accessory),所有者设备(Owner device)通常是指具有Android系统的设备如手机等,且和附件做过关联(配置),是配件的所有者,Google服务器和其它支持Google FMDN功能的手机或平板等Android设备。

做过关联的附件,在Bluetooth LE断开连接之后,会发出一种特定格式的广播。广播包含服务的UUID和一个叫EID(Ephemeral identifier)的数据。周围支持Google FMDN功能的手机或平板等Android设备,扫描并识别到这个特定的广播后,会通过EID生成一个密钥,将自己的GPS位置信息数据通过密钥加密之后传输给Google服务器,Google服务器会将这些数据存储起来。由于这些信息是加密的,Google服务器本身也识别不了,只有附件的所有者设备能解密数据从而获得附件的位置信息,别的设备即使获得了数据,也无法解密,确保了位置信息的安全性。
1.1 配件的主要功能
配件,主要是指定位标签(Locator Tags)类的设备。设备如果要获得谷歌的认证,必须支持“谷歌快速配对服务规范2.0” GFPS v2.0 specification中的相关章节,以及 “查找我的设备网络规范Find My Device Network specification”中相关章节,具体内容列于 “功能列表” 部分。这其中还包括相关的 “防止不必要跟踪Unwanted Tracking Prevention” 规范,该规范提供了一种跨平台的安全机制。
下面列出了对这类设备进行认证所需的必备功能和可选功能。原文网址:https://developers.google.com/nearby/fast-pair/specifications/devicefeaturerequirement/devicefeaturerequirement_locatortags
必须支持的功能:
可选的功能:
目前不支持的功能:
Nordic的Locator tag例子里面实现了所有必需的功能,例子介绍可以访问:
https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/samples/bluetooth/fast_pair/locator_tag/README.html#fast-pair-locator-tag
本篇博客代码部分的介绍,主要基于nRF Connect SDK2.9.0
1.2 配件广播
配件处于不同状态时,广播的内容有所不同。有三种广播,分别是快速配对可发现广播(Fast Pair Discoverable Advertising),快速配对不可发现广播(Fast Pair not Discoverable Advertising)和FMDN frame广播。Fast Pair Discoverable Advertising广播连接后,用于设备配置(关联);Fast Pair not Discoverable Advertising广播连接后用于时钟同步。配置(关联)成功之后的设备,断开连接后,进行FMDN frame广播。配件复位后,按下button1(nRF52840 DK),开始Fast pair广播。如果配件上已经存储了account key,则进行Fast Pair not Discoverable Advertising;否则,则进行Fast Pair Discoverable Advertising。如下图:

关于Fast Pair广播的内容可以参考前一篇博客。FMDN frame广播在规范里的定义如下:


选择的椭圆曲线方程不同,计算出来的ephemeral identifier长度就不同,FMDN frame的长度也不同。例子默认选择了SECP160R1(CONFIG_BT_FAST_PAIR_FMDN_ECC_SECP160R1=y)
choice BT_FAST_PAIR_FMDN_ECC_TYPE
prompt "Elliptic Curve selection"
default BT_FAST_PAIR_FMDN_ECC_SECP160R1
help
Select the Elliptic Curve used for encryption in FMDN modules.
config BT_FAST_PAIR_FMDN_ECC_SECP160R1
bool "FMDN with the SECP160R1 Elliptic Curve"
depends on BT_FAST_PAIR_CRYPTO_SECP160R1_SUPPORT
help
Select the SECP160R1 Elliptic Curve for FMDN. This curve type
is less secure than the SECP256R1 variant, but it can be used with
Bluetooth Legacy Advertising because of its limited size (160 bits).
config BT_FAST_PAIR_FMDN_ECC_SECP256R1
bool "FMDN with the SECP256R1 Elliptic Curve"
depends on BT_FAST_PAIR_CRYPTO_SECP256R1_SUPPORT
help
Select the SECP256R1 Elliptic Curve for FMDN. This curve type
is more secure than the SECP160R1 variant but requires using
Bluetooth Extended Advertising due to its extended size (256 bits).
EID(ephemeral identifier)的计算方法如下:
1)计算一个random数据:通过对如下数据结构的数据,用ephemeral identity key和AES-ECB-256算法加密后产生一个伪随机数,记作r'

Note: The device is assumed to have a 32-bit time counter in seconds.
设备提供一个以秒为单位的32位时间计数器。在将其放入数据结构之前,需要将其转换为32位大端字节序格式,并且将最低的K(这里是10)位清零。这意味着时间计数器的精度被降低到了2^10秒(即1024秒)的倍数。
Rotation period exponent is fixed and set to 10, corresponding to 1024 seconds. 该指数K是固定的,设置为 10,对应于 1024 秒。
2)选择SECP160R1或SECP256R1用于椭圆曲线加密操作。有关曲线定义,请参考《SEC 2:推荐的椭圆曲线域参数》,它定义了接下来运算所涉及的Fp,n和G。(https://www.secg.org/SEC2-Ver-1.0.pdf)
3)r = r' mod n(模运算是为了保证r小于基点G的阶n, 基点 G 的阶 n 是指最小的正整数,使得 nG 等于椭圆曲线上的无穷远点。
4)计算R = r* G,R是曲线上的一个点,它代表正在使用的公钥。(FMDN frame)信标将R的x坐标Rx作为其临时标识符(ephemeral identifier)进行广播。
椭圆曲线加密计算对应的主要代码如下:
int fp_crypto_ecc_secp160r1_calculate(uint8_t *out,
uint8_t *mod,
const uint8_t *in,
size_t datalen)
{
/* SECP160R1 key length corresponds to one coordinate. */
uint8_t public_key[FP_CRYPTO_ECC_SECP160R1_KEY_LEN * 2];
ocrypto_sc_p160 mod_le;
/* Verify the input length. */
if (datalen != SECP160R1_DATA_LEN) {
return -ENOTSUP;
}
/* Compute the intermediate modulo result.
* Store it in the output buffer after little-endian to big-endian conversion.
*/
ocrypto_sc_p160_from32bytes_alt(&mod_le, in);
sys_memcpy_swap(mod, mod_le.w, FP_CRYPTO_ECC_SECP160R1_MOD_LEN);
/* Calculate the public key and store the X coordinate in the output buffer. */
ocrypto_p160_scalar_mult_alt(public_key, in);
memcpy(out, public_key, FP_CRYPTO_ECC_SECP160R1_KEY_LEN);
return 0;
}
int fp_crypto_ecc_secp256r1_calculate(uint8_t *out,
uint8_t *mod,
const uint8_t *in,
size_t datalen)
{
ocrypto_cp_p256 public_key;
ocrypto_sc_p256 mod_le;
/* Verify the input length. */
if (datalen != SECP256R1_DATA_LEN) {
return -ENOTSUP;
}
/* Compute the intermediate modulo result.
* Store it in the output buffer after little-endian to big-endian conversion.
*/
(void) ocrypto_sc_p256_from32bytes(&mod_le, in);
sys_memcpy_swap(mod, mod_le.w, FP_CRYPTO_ECC_SECP256R1_MOD_LEN);
/* Calculate the public key and store the X coordinate in the output buffer. */
(void) ocrypto_curve_p256_scalarmult_base(&public_key, &mod_le);
ocrypto_curve_p256_to32bytes(out, &public_key);
return 0;
1.3 FMDN设备(配件)应用所涉及的密钥
- Account key: 账户密钥是Google fast pair过程中Seeker传给Provider的16字节的密钥。
- Owner account key:所有者账户密钥是从Account key list中选出来的一个account key。对于原本设计就是FMDN设备的情况,设备不支持Subsequent pairing,所以它只拥有一个account key,那么这个account key也是Owner account key。对于原本设计不是FMDN设备,且做过Google fast pair,拥有多个account key的情况,通过升级具有了FMDN能力,那么选择第一个account key作为Owner account key。
- Ephemeral identity key:临时身份密钥是Seeker做FMDN provisioning(配置)过程中,产生的一个32字节随机数。Seeker通过Beacon Actions Characteristic write方式,将EIK用owner account key做AES-ECB-128加密后,发送给Provider的。Provider使用owner account key解密EIK。 如果EIK被设备清除了,那么设备需要做工厂复位(factory reset)。EIK有两个用途:1. 参与SHA256计算,用于身份认证;2. 参与生成EID的计算。EID相当于椭圆曲线算法计算出来的一个公钥,是配件的FMND广播包数据的一部分。获得并存储了EIK的配件设备是provisioned设备。如果没有EIK,或是EIK被删除了,那么就是unprovision设备,不具备寻物网络功能。
- Recovery key:恢复临时身份密钥,对 “Ephemeral identity key ||(拼接) 0x01” 进行 SHA256 哈希运算,然后截取前 8 个字节。该密钥存储在配件上,并且在用户通过按下配件上的按钮表示同意的情况下,Seeker(所有者设备)可以使用带有它生成的安全凭证的命令来读取临时身份密钥 。
- Ring key:响铃密钥,对 “Ephemeral identity key || 0x02” 进行 SHA256 哈希运算,然后截取前 8 个字节。该密钥存储在配件上,Seeker只能使用带有它生成的安全凭证的命令来让配件发出响铃(响铃操作)。
- Unwanted tracking protection key:防止不必要跟踪密钥,对 “Ephemeral identity key || 0x03” 进行 SHA256 哈希运算,然后截取前 8 个字节。该密钥存储在配件上,Seeker只能使用带有它生成的安全凭证的命令来激活防止不必要跟踪模式(unwanted tracking protection mode )。
- One-time Authentication Key:一次性身份验证密钥,可以理解为一个安全凭证。Seeker发送不同命令的时候携带不同的One-time Authentication Key
1.4 配件配置后的主要状态
- Synchronization mode:在已配置(Provisioned)状态下,配件在系统重启后(例如,因更换电池而重启)会尝试同步其信标时钟。在这种情况下,它会发起 Fast Pair not Discoverable Advertising,以表明安卓设备需要与定位标签进行同步。智能手机检测Fast Pair not Discoverable Advertising后,会连接到该配件,并交换必要的数据,以便将其信标时钟与已连接设备的当前时钟值进行同步。在成功同步之后,定位标签会停止Fast Pair not Discoverable Advertising。从这时起,该配件开始FMDN Beacon Advertising。另外,安卓设备采用了一种限流机制,以防止信标时钟同步在每 24 小时内发生超过一次。配置(Provisioning)操作也被视为一次时钟同步事件,并且在下一次同步尝试之前需要等待 24 小时的时间间隔。(同步是安卓设备发起的)
- Identification mode: 在已配置状态下,可以在有限时间内进入识别模式。此模式允许已连接的设备从定位标签读取识别信息。这类识别信息的一个例子是通过蓝牙读取蓝牙通用访问配置文件(GAP)中的设备名称。识别模式还允许读取在《检测不必要的位置追踪器(DULT)》规范中定义的标识符有效载荷。识别模式的超时由 DULT 子系统处理,引入这一机制是为了提高用户隐私保护程度。
- Recovery mode:在已配置状态下,可以在有限时间内进入恢复模式。此模式允许安卓设备从定位标签中找回丢失的配置密钥EIK
- Motion detector mode:在已配置状态下, 配件可以激活《检测不必要的位置追踪器(DULT)》规范中定义的运动检测模式。当配件与所有者分离足够长的时间后(请参考配置选项:CONFIG_DULT_MOTION_DETECTOR_SEPARATED_UT_TIMEOUT_PERIOD_MIN 和 CONFIG_DULT_MOTION_DETECTOR_SEPARATED_UT_TIMEOUT_PERIOD_MAX),该模式将被激活。在这种状态下,如果检测到运动,配件就会启动响铃操作。发出的声音有助于提醒非所有者,他们携带了一个不属于自己的配件,并且原所有者可能会利用这个配件来追踪他们的位置。在DK上,可以通过按下按钮来模拟产生运动。在 Thingy:53 设备上,内置的加速度计被用于检测运动。在播放10次声音后,或者在检测到运动且过去20秒之后,运动检测器将在配置选项CONFIG_DULT_MOTION_DETECTOR_SEPARATED_UT_BACKOFF_PERIOD 中所设置的时间段内处于停用状态。如果配件再次出现在其所有者附近,运动检测器也会被停用。
- factory reset:配件执行工厂复位操作后,发生工厂复位(factory reset),配置数据被删除,需要重新配置。配件启动(reset)的过程中按下Button4(nRF 52840 DK),执行factory reset。
1.5 配件必须包含的蓝牙服务
配件必须包含fast pair GATT service(UUID 0xFE2C)和Accessory non-owner GATT service(UUID 0x15190001-12F4-C226-88ED-2AC5579F2A85),如下图:

通过fast pair GATT service里面的Bluetooth LE特征可以完成对配件的配置以及实现所有者设备(如Android手机)对配件的操控;通过accessory non-owner GATT service里面的Bluetooth LE特征可以实现非所有者设备(如Android手机)对配件的操控。Accessory non-owner GATT service用于实现DULT(Detecting Unwanted Location Trackers )协议,用来解决非法跟踪的问题Unwanted tracking prevention。
关于配件的配置主要包含两个阶段(一次连接就可以完成):第一阶段是通过Google fast pair获取account key,第二阶段是做provisioning获取临时身份密钥EIK(Ephemeral identity key)。关于第一阶段的详细介绍可以参考前一篇博客:Google快速配对服务(Google Fast Pair Service)详解和应用;第二阶段的配置(provisioning)过程,通过Beacon actions这个Bluetooth LE特征作为逻辑通道,由所有者设备发送Beacon Provisioning Request命令开始触发配置(provisioning)过程。Beacon actions的定义如下:

基于Beacon actions通道的每一个命令的通信流程如下:

Seeker(所有者设备)进行Bluetooth LE读操作的目的是从Provider(配件)处获得一个Nonce随机数和协议主版本号,用来生成one-time authentication key,用于身份认证;
Seeker 通过Bluetooth LE写请求命令给Provider发送命令,Provider通过Notification的方式发送命令响应。Beacon actions特性的代码定义:
/* Beacon Actions GATT Characteristic definition for the Fast Pair service. */
#define FP_FMDN_BEACON_ACTIONS_CHARACTERISTIC \
BT_GATT_CHARACTERISTIC(BT_FAST_PAIR_UUID_BEACON_ACTIONS, \
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY, \
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, \
fp_fmdn_beacon_actions_read, \
fp_fmdn_beacon_actions_write, \
NULL), \
BT_GATT_CCC(fp_fmdn_beacon_actions_ccc_cfg_changed, \
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE)
Bluetooth LE读操作响应callback函数如下,函数里提供了Nonce随机数和协议主版本号
ssize_t fp_fmdn_beacon_actions_read(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
void *buf,
uint16_t len,
uint16_t offset)
{
int err;
ssize_t res;
struct conn_context *conn_context;
uint8_t rsp[BEACON_ACTIONS_READ_RSP_LEN];
BUILD_ASSERT((sizeof(conn_context->random_nonce) +
BEACON_ACTIONS_READ_RSP_VERSION_LEN) ==
BEACON_ACTIONS_READ_RSP_LEN);
/* It is assumed that this callback executes in the cooperative thread context. */
__ASSERT_NO_MSG(!k_is_preempt_thread());
__ASSERT_NO_MSG(!k_is_in_isr());
LOG_DBG("Beacon Actions GATT Read Request");
/* Do not perform any action if Fast Pair is not ready. */
if (!bt_fast_pair_is_ready()) {
res = BT_GATT_ERR(BT_ATT_ERR_UNLIKELY);
LOG_INF("Beacon Actions read: res=%d conn=%p, "
"Return error because Fast Pair is not enabled", res, (void *)conn);
return res;
}
conn_context = &conn_contexts[bt_conn_index(conn)];
err = sys_csrand_get(conn_context->random_nonce, sizeof(conn_context->random_nonce));
if (err) {
LOG_ERR("Beacon Actions: failed to generate random nonce: err=%d", err);
return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY);
}
rsp[0] = CONFIG_BT_FAST_PAIR_FMDN_VERSION_MAJOR;
memcpy(&rsp[BEACON_ACTIONS_READ_RSP_VERSION_LEN],
conn_context->random_nonce,
sizeof(conn_context->random_nonce));
res = bt_gatt_attr_read(conn, attr, buf, len, offset, rsp, sizeof(rsp));
if (res == sizeof(rsp)) {
conn_context->is_challenge_valid = true;
LOG_HEXDUMP_DBG(conn_context->random_nonce, sizeof(conn_context->random_nonce),
"Beacon Actions: challenge-response enabled for the next write:");
}
LOG_DBG("Beacon Actions read: res=%d conn=%p", res, (void *)conn);
return res;
}
1.6 基于Beacon actions特性的命令
1.6.1 信标配置请求命令(Beacon provisioning request)
配置过程包括fast pair和provisioning两个阶段。Provisioning过程主要围绕Beacon provisioning request和response过程进行。信标配置请求命令格式如下:

信标配置请求命令含4个子命令:读取信标参数,读取配置状态,设置EIK和清除EIK
信标配置请求命令的一次性认证密钥(One-time authentication key)采用 HMAC-SHA256 算法计算结果并取结果的前 8 个字节的数据。HMAC(Hash - based Message Authentication Code,基于哈希的消息认证码)是一种通过使用哈希函数(这里是 SHA256)来生成消息认证码的机制。它结合了一个密钥(account key)和待认证的数据(协议主版本号 || 从该特性读取的最后一个随机数 || 数据 ID || 数据长度 || 附加数据,注:“||” 表示拼接操作),从而生成一个固定长度的哈希值。协议主版本号和随机数是对Beacon actions特征读操作过程中获取的。
1)读取信标参数(Read beacon parameters):Seeker发送读取信标参数(Read beacon parameters)命令后,配件设备返回参数信息。参数定义如下:

Calibrated power:在距离为 0 米处接收到的校准功率(取值范围在 [-100, 20] 之间)。以有符号整数表示,分辨率为 1 分贝毫瓦(dBm)。
Clock value:当前的时钟值(以秒为单位,大端字节序)。主要用于时间同步。
Curve selection:选择用于加密的椭圆曲线:
- 0x00(默认值):SECP160R1
- 0x01:SECP256R1(需要扩展广播)
Components:能够发出响铃声音的组件数量:
- 0x00:表示该设备无法发出响铃声音。
- 0x01:表示只有单个组件能够发出响铃声音。
- 0x02:表示左右耳塞这两个组件能够独立发出响铃声音。
- 0x03:表示左右耳塞以及充电盒这三个组件能够独立发出响铃声音。
Ringing capabilities:响铃能力:
- 0x00:不支持响铃音量选择。
- 0x01:支持响铃音量选择。如果设置为此值,提供者必须接受并处理如 “响铃操作” 中所示的 3 种音量级别。
Calibrated power计算函数如下:
int8_t fp_fmdn_state_tx_power_encode(void)
{
int32_t calibrated_tx_power;
__ASSERT_NO_MSG(bt_fast_pair_is_ready());
calibrated_tx_power = fmdn_adv_set_tx_power;
calibrated_tx_power += FMDN_TX_POWER_CORRECTION_VAL;
__ASSERT(calibrated_tx_power >= FMDN_TX_POWER_CALIBRATED_MIN,
"FMDN State: calibrated TX power is too low");
__ASSERT(calibrated_tx_power <= FMDN_TX_POWER_CALIBRATED_MAX,
"FMDN State: calibrated TX power is too high");
return calibrated_tx_power;
}
FMDN_TX_POWER_CORRECTION_VAL的值可以通过如下配置选项进行配置。配置值与从蓝牙低功耗(Bluetooth LE)控制器读取的发射功率(TX power)相加,以此计算 “读取信标参数” 响应中包含的校准发射功率值(Calibrated power)。之所以需要进行这种校正,是为了考虑硬件带来的影响(例如,所使用的天线或设备外壳)。
config BT_FAST_PAIR_FMDN_TX_POWER_CORRECTION_VAL
int "TX power correction value [dBm]"
default 0
help
The configured value is added to the TX power readout from Bluetooth
LE controller to calculate the calibrated TX power value included in
the Read Beacon Parameters response. The correction may be needed to
take into acccount hardware configuration (for example, used antenna
or device's casing).
The calibrated TX power included in the Read Beacon Parameters
response has to be in the following dBm range: [-100, 20].
Clock value的读取函数如下:
static uint32_t fmdn_clock_read(void)
{
int64_t sys_uptime;
uint32_t fmdn_clock;
/* Calculate elapsed time since bootup. */
sys_uptime = k_uptime_get();
/* Convert from milliseconds to seconds. */
sys_uptime /= MSEC_PER_SEC;
/* Cast the uptime value in seconds onto the uint32_t type.
* The uint32_t type of clock value should allow over a hundred
* years of operation without the overflow:
* 2^32 / 3600 / 24 / 365 ~ 136 years
*/
fmdn_clock = sys_uptime;
/* Calculate the absolute time using the checkpoint from NVM. */
fmdn_clock += storage_clock_boot_checkpoint;
return fmdn_clock;
}
Curve selection的读取函数如下:
uint8_t fp_fmdn_state_ecc_type_encode(void)
{
/* Define the encoding for the ECC configuration. */
enum ecc_type {
ECC_TYPE_SECP160R1 = 0x00,
ECC_TYPE_SECP256R1 = 0x01,
ECC_TYPE_INVALID = 0xFF,
};
__ASSERT_NO_MSG(bt_fast_pair_is_ready());
if (IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_ECC_SECP160R1)) {
return ECC_TYPE_SECP160R1;
}
if (IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_ECC_SECP256R1)) {
return ECC_TYPE_SECP256R1;
}
__ASSERT(0, "FMDN State: incorrect ECC type selection");
return ECC_TYPE_INVALID;
}
Components的读取函数如下:
uint8_t fp_fmdn_ring_comp_num_encode(void)
{
/* Define the encoding for the ringing components configuration. */
enum ring_comp {
RING_COMP_NONE = 0x00,
RING_COMP_ONE = 0x01,
RING_COMP_TWO = 0x02,
RING_COMP_THREE = 0x03,
};
__ASSERT_NO_MSG(bt_fast_pair_is_ready());
if (IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_RING_COMP_NONE)) {
return RING_COMP_NONE;
} else if (IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_RING_COMP_ONE)) {
return RING_COMP_ONE;
} else if (IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_RING_COMP_TWO)) {
return RING_COMP_TWO;
} else if (IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_RING_COMP_THREE)) {
return RING_COMP_THREE;
}
__ASSERT(0, "FMDN Ring: incorrect Ringing Components selection");
return 0;
}
Ringing capabilities的读取函数如下:
uint8_t fp_fmdn_ring_cap_encode(void)
{
/* Define the encoding for the ringing capabilities configuration. */
enum ring_cap {
RING_CAP_NONE = 0x00,
RING_CAP_VOLUME = 0x01,
};
__ASSERT_NO_MSG(bt_fast_pair_is_ready());
if (IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_RING_VOLUME)) {
return RING_CAP_VOLUME;
}
return RING_CAP_NONE;
Seeker除了配置过程需要发送读取信标参数命令外,同步模式(Synchronization mode)下也会发送读取信标参数命令,读取Clock Value,进行同步时钟。
2)读取配置状态(Read provisioning state):Seeker发送读取配置状态命令,来询问配件设备的配置状态。配件设备返回的数据格式如下:

设备(Provider)验证一次性认证密钥正确后,根据自己的状态(是否已存储EIK,和owner account key是否是当前account key),来设定响应的Provisioning state值,并填入当前的EID通过notify的方式发送给Seeker。
具体的处理过程可以参考函数:
static ssize_t provisioning_state_read_handle(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
struct net_buf_simple *req_data_buf)
3)设置EIK(Set ephemeral identity key)
通常在配置期间或EIK被更新后,Seeker发出此命令将加密后的EIK值传递给Provider设备。Seeker传递数据的additional data部分的数据: 32 bytes that are the ephemeral identity key, AES-ECB-128 encrypted with the account key. If the Provider already has an ephemeral identity key set, also send the first 8 bytes of SHA256(current ephemeral identity key || the last nonce read from the characteristic)
32 个字节,即临时身份密钥EIK,使用账户密钥进行 AES-ECB-128 加密。如果Provider已经设置过了临时身份密钥,则同时发送 SHA256 的前 8 个字节(当前临时身份密钥 || 从特征中读取的最后一个随机数)。这种情况是更改EIK的情况,需要验证两次。一次性认证密钥认证一次,SHA256哈希值再验证一次。验证无误后,新的EIK需要写入到Provider设备中, 新的EIK在 Bluetooth LE连接终止后立即生效。
具体的处理过程可以参考函数:
static ssize_t ephemeral_identity_key_set_handle(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
struct net_buf_simple *req_data_buf)
4)清除EIK(Clear ephemeral identity key)
如果Seeker设备要移除Provider设备,解除它们之间的配置关系,可以发送这条命令。Seeker传递数据的additional data部分的数据: The first 8 bytes of SHA256(ephemeral identity key || the last nonce read from the characteristic) SHA256(临时身份密钥 || 从特征值读取的最后一个随机数)结果的前 8 个字节。所以Provider需要验证两次,一次是一次性认证密钥认证,一次是SHA256哈希值验证。确保了Seeker要删除的EIK和Provider拥有的EIK是同一个EIK。验证通过后,Provider删除EIK,并立刻停止FMDN frames广播。
具体的处理过程可以参考函数:
static ssize_t ephemeral_identity_key_clear_handle(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
struct net_buf_simple *req_data_buf)
1.6.2信标配置密钥恢复请求命令(Beacon provisioning key recovery request)
如果Seeker的EIK丢失了,可以通过这条命令,从Provider设备上读取EIK。这个读取的过程是有条件的,Provider需要处于Recovery mode。协议建议用户执行一个操作进入Recovery mode 。我们例子是按下DK上的button4,按键时间3-7秒。Recovery mode持续一段时间后( CONFIG_BT_FAST_PAIR_FMDN_READ_MODE_FMDN_RECOVERY_TIMEOUT)自动退出。
命令格式如下:

Provider设备验证一次性认证密钥通过之后返回加密的EIK(32 bytes that are the ephemeral identity key, AES-ECB-128 encrypted with the account key)给Seeker。
具体的处理过程可以参考函数:
static ssize_t ephemeral_identity_key_read_handle(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
struct net_buf_simple *req_data_buf)
1.6.3响铃请求命令(Ringing request)
Seeker通过发送响铃请求命令控制Provider播放声音或获取响铃状态。格式如下:

有两个子命令,Ring和Read ringing state。Ring命令的additional data的数据如下:

对ring命令的回复数据格式如下:

Read ringing state命令没有additional data, 回复Read ringing state命令的数据格式如下:

Ring和Read ringing state命令的处理函数是
static ssize_t ring_handle(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
struct net_buf_simple *req_data_buf)
和
static ssize_t ringing_state_read_handle(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
struct net_buf_simple *req_data_buf)
1.6.4不必要的跟踪保护请求命令(Unwanted tracking protection request)
这个命令用于使能或禁止unwanted tracking protection功能。功能禁止的状态下, unwanted tracking protection相关的Bluetooth LE特征不允许被访问。命令格式如下:

命令处理函数是
static ssize_t activate_utp_mode_handle(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
struct net_buf_simple *req_data_buf)
和
static ssize_t deactivate_utp_mode_handle(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
struct net_buf_simple *req_data_buf)
1.6.5命令响应
Provider通过write response响应Seeker,通过notification返回数据给Seeker。
如何命令错误或是验证不通过,write response的GATT error code如下所示:

正常的notification返回数据的格式如下:

2. DULT( Detecting Unwanted Location Trackers)协议
DULT( Detecting Unwanted Location Trackers)是苹果和谷歌合作制定的一项适用于蓝牙跟踪设备的行业规范——‘发现不需要的位置跟踪器’,该规范可以跨iOS和安卓系统,提醒用户未知设备是否正在被用来跟踪他们。这将有助于减少追踪物品的设备(例如AirTag)被滥用。
在iPhone手机上,如果用户附近发现未知的蓝牙跟踪器,并且该跟踪器随其移动了一段时间,无论该设备与哪个平台配对,用户都会收到一条通知:“发现[物品]与您同行”。安卓用户则会收到类似的提醒:“跟踪器正与您同行”。
用户点击通知可以查看跟踪器的识别信息,并且支持播放声音来定位跟踪器。该功能还包含如何禁用跟踪设备的说明。
DULT协议规范:https://datatracker.ietf.org/doc/draft-ietf-dult-accessory-protocol/
2.1 FMDN协议对DULT的规范
Google的FMDN协议规范对DULT有具体的要求,解释网址如下:
https://developers.google.com/nearby/fast-pair/specifications/extensions/fmdn#unwanted-tracking-prevention
Relevant guidelines specific to FMDN to be compliant with DULT spec:
为符合 DULT 规范,“查找我的设备网络”(FMDN)相关的具体准则如下:
- Any FMDN compatible device must be registered in the Nearby Device Console, and have the "Find My Device" capability activated.
- 任何与 “查找我的设备网络”(FMDN)兼容的设备都必须在 “Nearby Device Console” 中注册,并激活 “Find My Device” 功能。
- The device must implement the Accessory Non-Owner service and characteristic defined in the implementation version of the DULT spec, including the Accessory Information operations and Non-owner controls.
- 设备必须实现 DULT 规范实施版本中定义的 “配件非所有者服务” 及特性,包括 “配件信息” 操作和 “非所有者控制”
- During the backward compatibility period, as defined in the DULT spec, there are no changes to the advertised frame as defined in this document.
- 在 DULT 规范所定义的向后兼容期内,本文档中定义的广播帧不得进行更改。
- "Unwanted tracking protection mode" defined in this document maps to the "separated state" defined by the DULT spec.
- 本文档中定义的 “防止不必要追踪模式” 与 DULT 规范中定义的 “分离状态” 相对应。
- Guidelines for implementing the Accessory Information opcodes:
- Get_Product_Data should return the model ID provided by the console, zero padded to fit the 8-byte requirement. For example, model ID 0xFFFFFF is returned as 0x0000000000FFFFFF.
- “获取产品数据”(Get_Product_Data)操作应返回控制台提供的型号标识,并且需用零填充以满足 8 字节的要求。例如,型号标识为 0xFFFFFF 时,应返回为 0x0000000000FFFFFF。
- Get_Manufacturer_Name and Get_Model_Name should match the values provided in the console.
- “获取制造商名称”(Get_Manufacturer_Name)和 “获取型号名称”(Get_Model_Name)操作返回的内容应与控制台中提供的值一致。
- Get_Accessory_Category can return the generic "Location Tracker" value if no other category better fits the type of the device.
- 如果没有更适合该设备类型的类别,“获取配件类别”(Get_Accessory_Category)操作可返回通用的 “位置追踪器” 这一值。
- Get_Accessory_Capabilities must indicate the support for ringing as well as BLE identifier lookup.
- “获取配件功能”(Get_Accessory_Capabilities)操作必须表明设备支持响铃功能以及Bluetooth LE(BLE)标识符查找功能。
- Get_Network_ID should return Google's identifier (0x02).
- “获取网络标识”(Get_Network_ID)操作应返回谷歌的标识(0x02)。
- Guidelines for implementing the Get_Identifier opcode:
- The operation should only return a valid response for 5 minutes after the user activated the 'identification' mode, which requires a combination of button presses. A visual or audio signal should indicate to the user that the provider entered that mode. The model-specific instructions for activating that mode must be provided to Google as a requirement for certification and at least 10 days prior to any update or modification to the instructions.
- 只有在用户激活 “识别” 模式后的 5 分钟内,该操作才应返回有效响应。激活 “识别” 模式需要组合按下按钮。应通过视觉或音频信号向用户表明设备已进入该模式。激活该模式的具体型号操作说明必须作为认证要求提供给谷歌,并且在对操作说明进行任何更新或修改之前至少 10 天提供。
- The response is constructed as: the first 10 bytes of current ephemeral identifier, followed by the first 8 bytes of HMAC-SHA256(recovery key, the truncated current ephemeral identifier).
- 响应的结构为:当前临时标识符(EID)的前 10 个字节,接着是使用 HMAC-SHA256 算法(基于恢复密钥和截断后的当前临时标识符EID)生成结果的前 8 个字节。
- Guidelines for implementing Identifier over NFC:
- As a URL, use find-my.googleapis.com/lookup.
- 网址方面,使用find-my.googleapis.com/lookup。
- As the e parameter, use the same response as constructed for Get_Identifier, hex encoded.
- 对于 “e” 参数,使用与 “获取标识符”(Get_Identifier)操作构建的响应相同的内容,并进行十六进制编码。
- As the pid parameter, use the same response as constructed for Get_Product_Data, hex encoded.
- 对于 “pid” 参数,使用与 “获取产品数据”(Get_Product_Data)操作构建的响应相同的内容,并进行十六进制编码。
- It is mandatory for the device to include a sound maker and support the ringing function. Per the DULT spec, the sound maker must emit a sound with minimum 60 Phon peak loudness as defined by ISO 532-1:2017.
- 设备必须配备发声装置并支持响铃功能。根据《检测不必要位置追踪器》(DULT)规范,发声装置必须发出符合 ISO 532-1:2017 标准所定义的、峰值响度至少为 60 方(Phon)的声音。
- Guidelines for implementing the Sound_Start opcode:
- The command should trigger ringing in all available components.
- 该命令应触发所有可用组件发出铃声。
- The maximal supported volume should be used.
- 应使用设备支持的最大音量。
- The recommended duration for ringing is 12 seconds.
- 铃声的推荐持续时长为 12 秒。
- Locator tags must include a mechanism that lets users temporarily stop advertising without factory resetting the device (for example, pressing a combination of buttons).
- 定位标签必须具备一种机制,使用户能够在不将设备恢复出厂设置的情况下暂时停止广播(例如,通过组合按键操作)
- The disablement instructions must be documented in a publicly available URL and provided to Google as a requirement for certification and at least 10 days prior to any update or modification to the instructions.
- 禁用说明必须记录在一个公开可访问的网址上,并作为认证要求提供给谷歌,且在对该说明进行任何更新或修改之前至少 10 天提供。
- The URL should support localization. Depending on the client, the language will be provided either as a query param ("hl=en") or using the "accept-language" HTTP header.
- 该网址应支持本地化。根据客户端的不同,语言信息将通过查询参数(如 “hl=en”)或使用 “accept-language” HTTP 标头来提供。
2.2 DULT在Nordic平台上的实现
DULT的模块位于: nrf\subsys\dult,通过设置CONFIG_DULT=y可以添加此模块。
Bluetooth LE服务定义如下:
/* Accessory non-owner GATT service UUIDs defined by the DULT specification. */
#define BT_UUID_ACCESSORY_NON_OWNER_SERVICE \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x15190001, 0x12F4, 0xC226, 0x88ED, 0x2AC5579F2A85))
#define BT_UUID_ACCESSORY_NON_OWNER_CHARACTERISTIC \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x8E0C0001, 0x1D68, 0xFB92, 0xBF61, 0x48377421680E))
BT_GATT_SERVICE_DEFINE(dult_accessory_non_owner_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_ACCESSORY_NON_OWNER_SERVICE),
BT_GATT_CHARACTERISTIC(BT_UUID_ACCESSORY_NON_OWNER_CHARACTERISTIC,
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_INDICATE,
BT_GATT_PERM_WRITE,
NULL, write_accessory_non_owner, NULL),
BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
);
通过Bluetooth LE特性作为逻辑通道,手机设备和配件设备(Accessory)之间完成信息的交互。
1)手机通过Accessory Information write commands获取Accessory的信息。Accessory通过Indication的方式返回数据。
2)手机通过Accessory Information write commands给Accessory发送控制命令。Accessory通过Indication的方式返回命令响应。
NON-OWNER CHARACTERISTIC只有在DULT功能使能的情况下才能被访问。FMDN frame广播的第7个字节的数据指示了配件设备是否支持DULT功能。

/* FMDN Frame type with Unwanted Tracking Protection Mode indication. */
#define FMDN_FRAME_TYPE_UTP_MODE_OFF 0x40
#define FMDN_FRAME_TYPE_UTP_MODE_ON 0x41
2.2.1 获取配件设备信息
手机需要从配件设备上获取这些信息:产品数据(Product data),制造商名称(Manufacturer name),型号名称(Model name),配件类别(Accessory category),协议实现版本(Protocol implementation version),配件功能(Accessory capabilities),网络标识(Network ID),固件版本(Firmware version),电池类型(Battery Type)和电池电量(Battery level)
Bluetooth LE特性写操作发送的读取命令的操作码如下:
/* Accessory non-owner GATT service UUIDs defined by the DULT specification. */
#define BT_UUID_ACCESSORY_NON_OWNER_SERVICE \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x15190001, 0x12F4, 0xC226, 0x88ED, 0x2AC5579F2A85))
#define BT_UUID_ACCESSORY_NON_OWNER_CHARACTERISTIC \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x8E0C0001, 0x1D68, 0xFB92, 0xBF61, 0x48377421680E))
BT_GATT_SERVICE_DEFINE(dult_accessory_non_owner_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_ACCESSORY_NON_OWNER_SERVICE),
BT_GATT_CHARACTERISTIC(BT_UUID_ACCESSORY_NON_OWNER_CHARACTERISTIC,
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_INDICATE,
BT_GATT_PERM_WRITE,
NULL, write_accessory_non_owner, NULL),
BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
);
配件设备需要保存和维护这些信息。这些信息放在一个叫dult_user的全局变量里面。
static const struct dult_user dult_user = {
.product_data = product_data,
.manufacturer_name = CONFIG_BT_FAST_PAIR_FMDN_DULT_MANUFACTURER_NAME,
.model_name = CONFIG_BT_FAST_PAIR_FMDN_DULT_MODEL_NAME,
.accessory_category = CONFIG_BT_FAST_PAIR_FMDN_DULT_ACCESSORY_CATEGORY,
.accessory_capabilities = (
(IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_DULT_CAPABILITY_PLAY_SOUND) ?
BIT(DULT_ACCESSORY_CAPABILITY_PLAY_SOUND_BIT_POS) : 0) |
(IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_DULT_CAPABILITY_MOTION_DETECTOR_UT) ?
BIT(DULT_ACCESSORY_CAPABILITY_MOTION_DETECTOR_UT_BIT_POS) : 0) |
(IS_ENABLED(CONFIG_BT_FAST_PAIR_FMDN_DULT_CAPABILITY_ID_LOOKUP_BLE) ?
BIT(DULT_ACCESSORY_CAPABILITY_ID_LOOKUP_BLE_BIT_POS) : 0)),
.network_id = DULT_NETWORK_ID_GOOGLE,
.firmware_version = {
.major = CONFIG_BT_FAST_PAIR_FMDN_DULT_FIRMWARE_VERSION_MAJOR,
.minor = CONFIG_BT_FAST_PAIR_FMDN_DULT_FIRMWARE_VERSION_MINOR,
.revision = CONFIG_BT_FAST_PAIR_FMDN_DULT_FIRMWARE_VERSION_REVISION,
},
除了Product data是Google注册时的model ID,存储在数据区,电池电量是读取的实际值,其余的数据基本都是宏的形式赋值。客户可以通过修改或配置宏来配置适合自己产品的数据。
配置宏的Kconfig文件位于:nrf\subsys\bluetooth\services\fast_pair\fmdn\Kconfig
2.2.2控制设备命令
控制命令主要用来控制声音(sound start和sound stop)和获取识别码(get Identifier)
Bluetooth LE特性写操作发送的控制命令的操作码如下:
/* DULT opcodes for Non-owner control writes. */
enum anos_chrc_non_owner_control_write_opcode {
ANOS_CHRC_NON_OWNER_CONTROL_WRITE_OPCODE_SOUND_START = 0x300,
ANOS_CHRC_NON_OWNER_CONTROL_WRITE_OPCODE_SOUND_STOP = 0x301,
ANOS_CHRC_NON_OWNER_CONTROL_WRITE_OPCODE_GET_ID = 0x404,
};
非所有者(Non-Owner)读取设备Identifier有个前提条件,就是设备要进入标识符读取状态(identifier read state),也就是前面提到的Identification mode。这需要用户执行一个用户操作。我们例子是按下DK上的button4,按键时间要小于3秒。
Identifier的值是EID的前 10 个字节,拼接使用 HMAC-SHA256 算法(基于恢复密钥recover key和截断后的EID)生成结果的前 8 个字节。
对应的函数是static int dult_id_payload_get(uint8_t *buf, size_t *len)
2.2.3 运动探测器(Motion Detector)
DULT规定配件应配备一个能够可靠检测配件运动的运动探测器(例如,加速度计)。如果配件包含加速度计,则必须对其进行配置,使其能够检测配件任意两个轴上 ±10° 的方向变化。
当配件设备与所有者分离足够长的时间时Motion Detector模式将被激活。在此状态下,如果检测到有运动,配件就会启动响铃动作。发出的声音有助于提醒非所有者,他们携带的这个配件不属于他们,原所有者可能会利用该配件来追踪他们的位置。
在 Thingy:53 设备上,内置的加速度计用于检测运动。在DK上,可以通过按钮操作来模拟产生运动。在播放 10 次声音后,或者在检测到运动且 20 秒过去之后,运动探测器将在 CONFIG_DULT_MOTION_DETECTOR_SEPARATED_UT_BACKOFF_PERIOD 中设置的时间段内停用。如果配件再次出现在其所有者附近,运动探测器也会停用。
DULT规范里针对Motion Detector定义了一些推荐的参数:

这些参数的配置宏的Kconfig文件位于nrf\subsys\dult\Kconfig
config DULT_MOTION_DETECTOR_SEPARATED_UT_SAMPLING_RATE1
int
default 10000
help
Sampling rate in milliseconds when motion detector is enabled in separated state.
config DULT_MOTION_DETECTOR_SEPARATED_UT_SAMPLING_RATE2
int
default 500
help
Motion detector sampling rate in milliseconds when movement is detected in separated
state.
config DULT_MOTION_DETECTOR_SEPARATED_UT_BACKOFF_PERIOD
int "Period in minutes to disable motion detector if accessory is in separated state" if DULT_MOTION_DETECTOR_TEST_MODE
default 2 if DULT_MOTION_DETECTOR_TEST_MODE
default 360
config DULT_MOTION_DETECTOR_SEPARATED_UT_TIMEOUT_PERIOD_MIN
int "Minimum time span in minutes in separated state before enabling motion detector" if DULT_MOTION_DETECTOR_TEST_MODE
default 3 if DULT_MOTION_DETECTOR_TEST_MODE
default 480
config DULT_MOTION_DETECTOR_SEPARATED_UT_TIMEOUT_PERIOD_MAX
int "Maximum time span in minutes in separated state before enabling motion detector" if DULT_MOTION_DETECTOR_TEST_MODE
default DULT_MOTION_DETECTOR_SEPARATED_UT_TIMEOUT_PERIOD_MIN if DULT_MOTION_DETECTOR_TEST_MODE
default 1440
motion detector模块对应的代码位于nrf\subsys\dult\motion_detector.c
总结
Nordic平台提供的代码实现了FMDN和DULT协议所必要的功能。另外,我们的Bluetooth LE协议栈稳定可靠,资源丰富,使用简单。如果客户还需要添加一些别的Bluetooth LE功能,得益于Nordic平台Bluetooth LE服务的模块化设计,用户自己能很方便的添加自己的或标准的Bluetooth LE服务模块。此外,我们在多链接和多广播集方面也表现优秀,很适合复杂Bluetooth LE设备的开发。