小米一碰传NFC Tag协议解析

流传的NDEF信息

目前网上流传最多的小米一碰传NFC NDEF信息大多为:

text 复制代码
d42a4d636f6d2e7869616f6d692e6d695f636f6e6e6563745f736572766963653a65787465726e616c747970650a4b0801100d2201032a094d492d4e4643544147380f4a31271700000008000e5441475f444953434f564552454465064d4952524f52011141383a36443a41413a31373a44373a35316a02fa7f

其中:

  • 00000008000被认为是设备类型:8为平板,3为电脑
  • 41383a36443a41413a31373a44373a3531被认为是蓝牙Mac地址的字符串

诚然这样的修改确实起效,但是这个NDEF信息中的其他部分是什么含义依然并不清晰。

因此必须逆向APP才能得到的准确含义,以下为解析过程和实际含义解释。

解析真实NDEF含义

NDEF解析

首先,NDEF信息中包含了NFC TAG需要的配置信息,这些信息都是可以通过NDEF协议进行解析的。

使用Python的ndef库,将以上HEX字符串转为bytes后可以得到如下信息:

text 复制代码
Record  0
	flags:
		message_begin: True
		message_end: True
		chunked: False
		short: True
		id: False
	tnf: 4 [EXTERNAL]
	type_len: 42
	type: b'com.xiaomi.mi_connect_service:externaltype'
	id_len: 0
	id: None
	payload_len: 77
	payload: 0a4b0801100d2201032a094d492d4e4643544147380f4a31271700000008000e5441475f444953434f564552454465064d4952524f52011141383a36443a41413a31373a44373a35316a02fa7f

一条NDEF数据被认为是一条Message,一个Message中包含了多个Record

经过解析,这条NDEF数据中只包含了一个Record,其载荷为外置(EXTERNAL)类型的com.xiaomi.mi_connect_service:externaltype

因此下一步需要找到安卓系统如何处理这样的NDEF内容。

处理NDEF信息的APP

根据安卓官方文档,要处理这种类型的数据,就需要APP在Manifestactivity中声明以下内容:

xml 复制代码
<intent-filter>
	<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
	<category android:name="android.intent.category.DEFAULT"/>
	<data android:scheme="vnd.android.nfc" android:host="ext" android:pathPrefix="/com.xiaomi.mi_connect_service:externaltype"/>
</intent-filter>

也就是说要处理Uri为vnd.android.nfc://ext/com.xiaomi.mi_connect_service:externaltype的NDEF内容。

使用ADB命令在手机上搜索可以得到com.xiaomi.mi_connect_service/.nfc.NfcFieldOnPublisher

shell 复制代码
adb shell cmd package query-activities --components -d "vnd.android.nfc://ext/com.xiaomi.mi_connect_service:externaltype"

找到com.xiaomi.mi_connect_service包名的App,名称为小米互联通信服务

查看其中的.nfc.NfcFieldOnPublisher组件,发现使用了com.xiaomi.mi_connect.nfc.proto.v1.NfcTagAdvDataCoder类对数据进行解析。

不难发现,这个NDEF TAG使用了ProtoBuf的数据存储格式,在APP的资源文件中进行对比可以找到AttrProto.proto文件,以下为部分截取的片段。

protobuf 复制代码
syntax = "proto3";

package mi_connect_service;

message AttrAdvData {
    int32 versionMajor = 1;
    int32 versionMinor = 2;
    bytes apps = 3;//已过时。请使用appIds替代。
    bytes flags = 4;
    string name = 5;
    bytes idHash = 6;
    int32 deviceType = 7;
    int32 securityMode = 8;
    repeated bytes appsData = 9;
    repeated bytes supportSetting = 10;
    repeated bytes currentSetting = 11;
    string wifiMac = 12;
    repeated int32 appIds = 13;//引入appIds之前使用的是apps
    int32 commData = 14;
    bool ziped = 15;
    string wiredMac = 16;//有线网卡mac
    string btMac = 17; //Bluetooth Device MAC
}

message AttrOps {
    AttrAdvData advData = 1;
    int32 sequenceId = 14;
}

使用Python将载荷中的数据使用ProtoBuf数据格式解析,可以得到以下JSON:

json 复制代码
{
	"advData": {
		"appIds": [
			16378
		],
		"appsData": [
			"JxcAAAAIAA5UQUdfRElTQ09WRVJFRGUGTUlSUk9SARFBODo2RDpBQToxNzpENzo1MQ=="
		],
		"deviceType": 15,
		"flags": "Aw==",
		"name": "MI-NFCTAG",
		"versionMajor": 1,
		"versionMinor": 13
	}
}

注:flagsappsData下的数据使用base64编码,实际为bytes

text 复制代码
flags: b'03'
app data: [b'27:17:00:00:00:08:00:0e:54:41:47:5f:44:49:53:43:4f:56:45:52:45:44:65:06:4d:49:52:52:4f:52:01:11:41:38:3a:36:44:3a:41:41:3a:31:37:3a:44:37:3a:35:31']

AttrAdvData字段含义

进一步解析APP相关代码,可以得到如下表格:

appId

数字 类型
1 MI_SHARE
2 MI_PLAY
62 MI_ACCOUNT
63 MI_MOVER
16382 MI_TEST
3 MI_MIRROR
16381 MI_REMOTE_CONTROLLER
16379 MI_VOIP
16377 MI_VIDEO_RELAY
16378 MI_TAP
16376 MI_WATCH_CAMERA

flags

数字 操作
1 发送 com.xiaomi.nfc.action.TAG_DISCOVERED 广播,使用ProtoBuf中的wifiMacbtMac数据
3 发送 com.xiaomi.nfc.action.[ACTION] 自定义ACTION广播,使用ProtoBuf中的appsData数据
Others 发送 com.xiaomi.aiot.nfc.message 本地广播,转发NDEF的TagRecord数据

注:接收 com.xiaomi.nfc.action.TAG_DISCOVERED 广播或者 com.xiaomi.nfc.action.[ACTION] 自定义ACTION广播,需要对应的BroadcastReceiver拥有 com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT 权限。

目前可用Action只有TAG_DISCOVERED

shell 复制代码
adb shell "dumpsys package resolvers receiver | grep com.xiaomi.nfc.action."

appsData数据含义

数据:27:17:00:00:00:08:00:0e:54:41:47:5f:44:49:53:43:4f:56:45:52:45:44:65:06:4d:49:52:52:4f:52:01:11:41:38:3a:36:44:3a:41:41:3a:31:37:3a:44:37:3a:35:31

数据 位置 含义
27:17 0 ~ 1 版本号,十进制为39 23,直接通过是否相等判断
00:00:00:08 2 ~ 5 设备类型,十进制为8,int32
00 6 数字键值对数据长度,此处为0表示无数据

数字键值对数据

由于没有真实数据所以只说明数据格式

位置 含义
0 键,整数
1 值长度
2 ~ 2+X-1 值,bytes

尾部

数据 位置 含义
0e 7 自定义ACTION广播的名称的长度,0x0e=14
54: ... :44 8 ~ 8+14-1 自定义ACTION广播的名称,此处含义TAG_DISCOVERED
65: ... :31 之后全部 发送的bytes载荷

自定义ACTION广播

由于这个NDEF只涉及flag=3的情况,所以本文只分析这个广播的参数构建

  • Action: com.xiaomi.nfc.action.[ACTION]
  • Permission: com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT
    解析 appsData 可得 com.xiaomi.nfc.action.TAG_DISCOVERED
  • Flags: 添加了多组flags
    • 16777216 安卓Flag Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP
    • 32 安卓Flag Intent.FLAG_INCLUDE_STOPPED_PACKAGES
    • 268435456 小米自定义Flag StaticConfigServiceConnector.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE
  • Extra:
    • protocol_value_key 内容为JSON
protocol_value_keyJSON构建
device_type_key 设备类型,将00:00:00:08转为int32,即8
attribute_value_key 数字键值对数据转JSON后,UTF-8编码为Base64的字符串
protocol_payload_key 发送的bytes载荷,转为的Base64的字符串

注:不存在某种数据就不存在这个JSON字段。

数字键值对数据转JSON:键数字转为字符串,目前值直接交给org.json.JSONObject处理(这样写会出BUG,不知道啥时候修复,可能用不到就不修复)。

处理NFC TAG信息的APP

对于以上Intent,使用ADB命令在手机上搜索可以得到com.milink.service/com.miui.circulate.nfc.relay.NfcTransportReceiver

shell 复制代码
adb shell cmd package query-receivers --components -a "com.xiaomi.nfc.action.TAG_DISCOVERED"

找到 com.milink.service 包名的App,名称为投屏或者互联互通服务

查看其中的 com.miui.circulate.nfc.relay.NfcTransportReceiver 组件,发现使用了 com.xiaomi.mi_connect.nfc.proto.v1.NfcTagAdvDataCoder 类对数据进行解析。

device_type_key数据

可以在这个APP中找到更多的设备类型定义:

数值 类型
2 TV
3 PC
5 CAR
8 PAD

protocol_payload_key数据

数据:65:06:4d:49:52:52:4f:52:01:11:41:38:3a:36:44:3a:41:41:3a:31:37:3a:44:37:3a:35:31

数据 位置 含义
65 0 键,整数,101
06 1 值长度,0x16=6
4d:49:52:52:4f:52 2 - 2+6-1 值,UTF-8字符串,MIRROR
01 9 键,整数,1
11 10 值长度,0x11=17
41: ... : 31 11 - 11+17-1 值,UTF-8字符串,A8:6D:AA:17:D7:51

键数值含义

数值 字段名 数据类型 解释
101 actionSuffix String Action后缀
1 btMac String 蓝牙Mac地址
2 wifiMac String WIFI Mac地址
3 wiredMac String 有线Mac地址
121 extAbility bytes 额外能力

actionSuffix

目前已知的actionSuffix为:

  • MIRROR
  • TVCAST
shell 复制代码
adb shell "dumpsys package resolvers receiver | grep com.miui.onehop.action."

根据actionSuffix发送自定义Action的广播

发送的是有序广播:OrderedBroadcast

  • Action: com.miui.onehop.action.[ACTION] 解析protocol_payload_key可得com.miui.onehop.action.MIRROR
  • Permission: com.miui.onehop.permission.MIRROR
  • Flags: 添加了多组flags
    • 32 安卓Flag Intent.FLAG_INCLUDE_STOPPED_PACKAGES
    • 16777216 小米自定义Flag RemoteCallAdapterKt.FLAG_RECEIVER_INCLUDE_BACKGROUND
  • Extra:
    • _device_type int,设备类型,device_type_key
    • _bt_mac String,蓝牙Mac地址,btMac
    • _wifi_mac String,WIFI Mac地址,wifiMac
    • _wired_mac String,有线Mac地址,wiredMac
    • _ability_lyra Boolean,(extAbility[0] & 1) > 0

这个广播目前只由Appcom.milink.service自己接收并处理。

至此NFC Tag中的NDEF数据就已经解析完毕,再向下分析已经没有必要了。

NDEF解析总结

其实使用流传的NDEF修改方法并无问题,只是在修改设备类型时存在不影响结果的错误。

至于其他的字段含义,对于NFC快速流转并无影响,了解数据结构可能对未来协议功能升级后的破解会更加方便。

因此简单来说,可以将NDEF进行如下修改即可:

text 复制代码
d42a4d636f6d2e7869616f6d692e6d695f636f6e6e6563745f736572766963653a65787465726e616c747970650a4b0801100d2201032a094d492d4e4643544147380f4a3127170000000[deviceType]000e5441475f444953434f564552454465064d4952524f520111[btMac]6a02fa7f

deviceType: 3 or 8
btMac: 4138 3a 3644 3a 4141 3a 3137 3a 4437 3a 3531

触发小米一碰传的其他方法

如果想要代码触发小米一碰传功能有两种方法:

  • 发送虚拟的NFC Tag的NDEF信息
  • 直接向投屏或者互联互通服务发送广播

发送虚拟的NFC Tag的NDEF信息

安卓中NDEF广播要求必须有android.nfc.extra.TAGTag数据,但是这个数据只有真实的NFC硬件才能生成。

因此一个安全的解决方案是,对于已有但不能修改的NFC Tag,可以使用代码获取到这个NFC Tag的硬件Tag数据,并对其中的内容修改后转发。

kotlin 复制代码
// 从Intent中获取已有的NDEF数据
val tag: Tag? = IntentCompat.getParcelableExtra(intent, NfcAdapter.EXTRA_TAG, Tag::class.java)
val id: ByteArray = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID)

val btMac = "00:00:00:00:00:00"
val hexPart1 = "0a4b0801100d2201032a094d492d4e4643544147380f4a3127170000000"
val hexPart2 = "000e5441475f444953434f564552454465064d4952524f520111"
val hexPart3 = "6a02fa7f"
// PC
val hex = hexPart1 + "3" + hexPart2 + btMac.toByteArray().toHexString() + hexPart3

val payload = hex.decodeHex()
val externalType = "com.xiaomi.mi_connect_service:externaltype"
val record = NdefRecord(NdefRecord.TNF_EXTERNAL_TYPE, externalType.toByteArray(), null, payload)

Intent(NfcAdapter.ACTION_NDEF_DISCOVERED).apply {
	data = Uri.parse("vnd.android.nfc://ext/$externalType")
	putExtra(NfcAdapter.EXTRA_TAG, tag)
	putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, arrayOf(NdefMessage(record)))
	putExtra(NfcAdapter.EXTRA_ID, id)
}.also {
	ContextCompat.startActivity(this, it, null)
}

另一方面,com.xiaomi.mi_connect_service小米互联通信服务)在处理收到的数据时,只有flags != 1flags != 3时才会获取并处理Tag

因此想要模拟这个广播,另一个不安全但是能用的方案就是不传递这个Tag数据,这样在当前使用场景下不会报错。

kotlin 复制代码
// 直接设置为空即可
val tag: Tag? = null
// 一个空的NFC Tag ID
val id: ByteArray = "000000000000".decodeHex()

// 类似的代码 ...

关于如何接收指定的NDEF数据,可以查看官方文档

投屏或者互联互通服务发送广播

模拟NDEF数据总可能会出现一些问题,因此另一个方案就是直接发送投屏的广播。

根据广播权限的定义,应用程序需要拥有com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT权限,才能向同样要求这个权限的广播接收器发送广播。

值得庆幸的是,这个权限定义在com.xiaomi.mi_connect_service小米互联通信服务)中并且没有任何的签名或者系统限制,所以可以直接在测试APP的Manifest中申明这个权限。

kotlin 复制代码
val btMac = "00:00:00:00:00:00"
val hex = "65064d4952524f520111" + btMac.toByteArray().toHexString()
val payload = Base64.encodeToString(hex.decodeHex(), Base64.DEFAULT)

val json = JSONObject()
json.put("device_type_key", 3) // PC
json.put("protocol_payload_key", payload)
val extra = json.toString()

sendBroadcast(Intent("com.xiaomi.nfc.action.TAG_DISCOVERED").apply {
	addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
	addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
	addFlags(268435456)
	putExtra("protocol_value_key", extra)
}, "com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT")

一碰传文件

触发一碰传文件的要求是文件/相册APP在前台,并且选中了要传输的文件。

如果想要触发,那么发送Intent的APP就必须在后台,或者不显示任何界面的发送完广播就退出。

Activity不显示界面可以使用以下Theme:

xml 复制代码
<activity android:theme="@android:style/Theme.NoDisplay" />

分析的版本

以上所有的分析过程建立在对HyperOS中提取出的APP上,并在小米电脑管家1.0.0.489上通过测试。

名称 包名 版本
小米互联通信服务 com.xiaomi.mi_connect_service 3.1.450.10
投屏 com.milink.service 15.0.4.1.e4a233b.2894359
NFC服务 com.android.nfc 14

本文首发于个人博客,并转载于简书

相关推荐
找藉口是失败者的习惯2 小时前
Jetpack Compose 如何布局解析
android·xml·ui
Estar.Lee7 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh7 小时前
uiautomator案例
android
工业甲酰苯胺8 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3438 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee10 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯10 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey12 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!13 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟14 小时前
Android音频采集
android·音视频