小米一碰传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

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

相关推荐
叽哥1 小时前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走1 小时前
创建自定义语音录制View
android·前端
用户2018792831671 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831671 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker3 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong3 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil4 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin
程序员码歌11 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端
书弋江山12 小时前
flutter 跨平台编码库 protobuf 工具使用
android·flutter
来来走走15 小时前
Flutter开发 webview_flutter的基本使用
android·flutter