英迈寰球网站建设网站建设 小程序开发 营销推广
英迈寰球网站建设,网站建设 小程序开发 营销推广,青年旅舍 wordpress 模版,网站qq一键登录Android NFC实战#xff1a;深入M1卡读写与安全应用开发
最近在整理工作室的旧设备时#xff0c;翻出了几张尘封已久的门禁卡和公交卡。看着这些小小的卡片#xff0c;我突然意识到#xff0c;它们背后所依赖的NFC技术#xff0c;其实早已深度融入我们的日常生活。从便捷的…Android NFC实战深入M1卡读写与安全应用开发最近在整理工作室的旧设备时翻出了几张尘封已久的门禁卡和公交卡。看着这些小小的卡片我突然意识到它们背后所依赖的NFC技术其实早已深度融入我们的日常生活。从便捷的交通支付到高效的门禁管理再到那些有趣的智能标签应用NFC正以一种“润物细无声”的方式改变着交互体验。作为一名开发者我们不应仅仅满足于使用更应理解其原理并亲手构建与之交互的应用。今天我们就抛开那些浅尝辄止的教程深入Android NFC开发的腹地聚焦于最常见的Mifare Classic 1K俗称M1卡从底层通信原理到上层应用开发构建一套完整、安全且实用的读写方案。1. 理解NFC与M1卡不只是“碰一碰”很多人对NFC的第一印象是手机支付时的“碰一碰”。这没错但它仅仅是冰山一角。NFC本质上是基于13.56MHz频率的近距离无线通信技术它脱胎于更早的RFID射频识别但增加了点对点通信模式。在Android生态中我们主要与三种NFC标签类型打交道NFC-A (ISO 14443 Type A)、NFC-B (ISO 14443 Type B)以及NFC-F (FeliCa)。而我们今天的主角——M1卡正是NFC-A阵营中的绝对主力。为什么M1卡如此普遍原因在于其经典的设计与足够的性价比。它采用非对称的加密与认证流程一度被认为是安全的尽管后来其加密算法Crypto-1已被破解但这并不妨碍其在门禁、校园卡、会员卡等对实时安全性要求并非极端严苛的领域继续大规模服役。理解它的存储结构是操作的第一步。一张标准的M1 S50卡1KB容量的存储空间被组织为16个扇区Sector 0-15每个扇区包含4个数据块Block 0-3每个块有16字节。这就像一栋16层的楼房每层有4个房间每个房间能放16件物品。扇区号块号主要用途与特点00厂商块。存储全球唯一的UID4字节或7字节及厂商信息只读不可更改。这是卡的“身份证”。01-2数据块。可存储用户数据。03扇区0的尾块控制块。存储密码A6字节、存取控制4字节、密码B6字节。1-150-2数据块。每个扇区的通用数据存储区。1-153各扇区的尾块控制块。存储该扇区独立的密码A、存取控制、密码B。这里的关键在于尾块Block 3。它决定了对应扇区的“门锁”规则。存取控制字节Access Bits的每一位都定义了对应数据块Block 0-2的读写、增减值权限以及验证密码A或密码B后才能执行的操作。出厂默认状态下许多卡的密钥A和密钥B都是0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF六个0xFF或者在某些读写器中默认使用0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7这个密钥。但在实际项目中你遇到的卡很可能已经被修改过密钥。注意直接读取尾块时密码区通常会返回全零0x00这是一种安全保护机制防止密码被轻易窃取并不代表真实密码就是零。2. 搭建Android NFC开发环境与权限配置在开始写代码之前我们需要确保开发环境就绪。首先你的测试设备必须支持NFC功能。其次在AndroidManifest.xml中声明必要的权限和特性。!-- 声明NFC权限 -- uses-permission android:nameandroid.permission.NFC / !-- 声明设备需要支持NFC -- uses-feature android:nameandroid.hardware.nfc android:requiredtrue / !-- 如果你的应用需要在前台优先处理NFC标签 -- uses-permission android:nameandroid.permission.VIBRATE /除了权限我们还需要在AndroidManifest.xml中为需要处理NFC的Activity配置intent-filter。这里有两种主流模式前台分发模式当你的应用处于前台时可以优先捕获NFC事件。这适合需要持续与卡片交互的场景如读写器应用。Intent过滤器模式当设备扫描到特定格式的NFC标签时系统会询问用户是否打开你的应用。这更适合标签触发类应用。对于我们的M1卡读写器采用前台分发模式更为灵活和即时。接下来我们将在主Activity中实现核心逻辑。3. 核心流程检测、连接与认证M1卡整个交互流程可以概括为发现卡片 - 建立连接 - 选择技术类型 - 循环进行扇区认证与数据操作 - 断开连接。让我们用代码一步步拆解。首先在Activity中初始化NFC适配器并启用前台分发。class NFCActivity : AppCompatActivity() { private lateinit var nfcAdapter: NfcAdapter private lateinit var pendingIntent: PendingIntent private var techLists: ArrayArrayString? null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) nfcAdapter NfcAdapter.getDefaultAdapter(this) if (nfcAdapter null) { Toast.makeText(this, 此设备不支持NFC, Toast.LENGTH_LONG).show() finish() return } // 创建PendingIntent用于在检测到标签时启动当前Activity pendingIntent PendingIntent.getActivity( this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0 ) // 指定我们只处理Mifare Classic标签 techLists arrayOf(arrayOf(android.nfc.tech.MifareClassic::class.java.name)) } override fun onResume() { super.onResume() // 启用前台分发让当前Activity优先接收NFC事件 nfcAdapter.enableForegroundDispatch(this, pendingIntent, null, techLists) } override fun onPause() { super.onPause() // 暂停时禁用前台分发避免影响其他应用 nfcAdapter.disableForegroundDispatch(this) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // 当检测到NFC标签时系统会调用此方法并传入新的Intent processIntent(intent) } private fun processIntent(intent: Intent) { val tag intent.getParcelableExtraTag(NfcAdapter.EXTRA_TAG) tag?.let { // 核心处理逻辑 handleTag(it) } } }当handleTag方法被调用时我们拿到了一个Tag对象。这是与物理卡片通信的入口。接下来我们获取MifareClassic对象它封装了所有针对M1卡的操作。private fun handleTag(tag: Tag) { val mfc MifareClassic.get(tag) try { mfc.connect() // 获取卡类型 val type mfc.type val typeString when (type) { MifareClassic.TYPE_CLASSIC - Mifare Classic MifareClassic.TYPE_PLUS - Mifare Plus else - Unknown } log(卡片类型: $typeString) // 获取存储容量和扇区数 val size mfc.size val sectorCount mfc.sectorCount log(存储容量: ${size}B, 扇区数: $sectorCount) // 读取UID厂商块数据 val uid tag.id val uidHex bytesToHex(uid) log(卡片UID: $uidHex) // 假设我们使用默认密钥A进行认证 val defaultKeyA byteArrayOf( 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte() ) // 遍历所有扇区尝试用默认密钥读取数据 for (sectorIndex in 0 until sectorCount) { // 认证扇区 val authResult mfc.authenticateSectorWithKeyA(sectorIndex, defaultKeyA) if (authResult) { log(扇区 $sectorIndex 认证成功) // 获取该扇区的第一个数据块通常是该扇区的Block 0 val firstBlockOfSector mfc.sectorToBlock(sectorIndex) // 读取该块数据 val blockData mfc.readBlock(firstBlockOfSector) log(扇区${sectorIndex} Block${firstBlockOfSector} 数据: ${bytesToHex(blockData)}) } else { log(扇区 $sectorIndex 认证失败可能密钥不正确) } } mfc.close() } catch (e: IOException) { log(与卡片通信时发生IO异常: ${e.message}) } catch (e: FormatException) { log(数据格式异常: ${e.message}) } } // 辅助函数字节数组转十六进制字符串 private fun bytesToHex(bytes: ByteArray): String { val hexArray 0123456789ABCDEF.toCharArray() val hexChars CharArray(bytes.size * 2) for (j in bytes.indices) { val v bytes[j].toInt() and 0xFF hexChars[j * 2] hexArray[v ushr 4] hexChars[j * 2 1] hexArray[v and 0x0F] } return String(hexChars) }这段代码演示了最基本的连接、认证和读取流程。但现实情况往往更复杂密钥不是默认的。这就需要我们引入密钥管理和扇区遍历策略。4. 高级实战密钥管理与安全读写策略直接使用默认密钥在很多实际场景下会碰壁。一个健壮的M1卡读写应用应该具备密钥管理功能。我们可以设计一个简单的密钥库支持添加、编辑和尝试多个密钥。首先定义一个数据类来存储密钥信息data class SectorKey( val keyType: Int, // MifareClassic.KEY_A 或 MifareClassic.KEY_B val keyValue: ByteArray, val alias: String // 密钥别名如“默认密钥”、“门禁扇区密钥” ) { override fun equals(other: Any?): Boolean { if (this other) return true if (javaClass ! other?.javaClass) return false other as SectorKey if (keyType ! other.keyType) return false if (!keyValue.contentEquals(other.keyValue)) return false return true } override fun hashCode(): Int { var result keyType result 31 * result keyValue.contentHashCode() return result } }然后构建一个密钥尝试器。它的逻辑是对于一个扇区依次尝试密钥库中的所有密钥包括A密钥和B密钥直到认证成功或全部失败。class KeyManager { private val keyStore mutableListOfSectorKey() init { // 预置一些常见密钥 keyStore.add(SectorKey(MifareClassic.KEY_A, byteArrayOf(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF), 默认密钥A)) keyStore.add(SectorKey(MifareClassic.KEY_A, byteArrayOf(0xD3.toByte(), 0xF7.toByte(), 0xD3.toByte(), 0xF7.toByte(), 0xD3.toByte(), 0xF7.toByte()), 常用密钥A)) keyStore.add(SectorKey(MifareClassic.KEY_B, byteArrayOf(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF), 默认密钥B)) } fun addKey(key: SectorKey) { if (!keyStore.contains(key)) { keyStore.add(key) } } fun authenticateSector(mfc: MifareClassic, sectorIndex: Int): SectorKey? { for (key in keyStore) { val success when (key.keyType) { MifareClassic.KEY_A - mfc.authenticateSectorWithKeyA(sectorIndex, key.keyValue) MifareClassic.KEY_B - mfc.authenticateSectorWithKeyB(sectorIndex, key.keyValue) else - false } if (success) { return key } } return null } }现在我们可以重构handleTag方法使用KeyManager进行智能认证并实现更安全的数据写入。写入数据前务必检查该数据块的存取控制位确认当前认证的密钥是否拥有写权限。一个鲁棒的写入函数应该包含权限检查。private fun writeDataToBlock(mfc: MifareClassic, sectorIndex: Int, blockIndexInSector: Int, data: ByteArray): Boolean { // 计算绝对块号 val absoluteBlockIndex mfc.sectorToBlock(sectorIndex) blockIndexInSector // 确保要写入的是数据块不是厂商块或控制块 if (absoluteBlockIndex 0 || blockIndexInSector 3) { log(错误不能写入厂商块或控制块。) return false } // 数据长度必须是16字节 if (data.size ! 16) { log(错误写入数据必须是16字节。) return false } try { mfc.writeBlock(absoluteBlockIndex, data) log(成功写入块 $absoluteBlockIndex) // 立即读取验证 val verifyData mfc.readBlock(absoluteBlockIndex) if (data.contentEquals(verifyData)) { log(写入验证成功) return true } else { log(警告写入验证失败数据可能未正确写入。) return false } } catch (e: IOException) { log(写入失败: ${e.message}) return false } }提示在实际对卡片进行写操作前强烈建议先进行一次完整的扇区数据备份。你可以将每个扇区每个块的数据读取出来保存为本地文件。这样即使操作失误也有机会恢复。5. 解析复杂应用场景公交卡与门禁卡数据模拟掌握了基础的读写我们就可以探索一些有趣的应用场景比如分析公交卡或门禁卡的数据结构。请注意此部分内容仅用于技术研究和学习请严格遵守相关法律法规和用户协议切勿用于非法复制或侵犯他人财产。门禁卡的数据通常存储在某个或某几个扇区中。通过上一节的遍历读取你可以看到哪些扇区有非零数据。这些数据可能包含用户ID或卡号有时是明文的有时是加密的有效期时间戳权限信息如可进入的楼层、区域校验和用于防止数据被篡改例如你可能会发现扇区1的数据块0存储着0x01 0x23 0x45 0x67 0x89 0xAB 0xCD 0xEF ...这样的数据前8个字节可能就对应着你的门禁卡号。但更常见的情况是这些数据经过了简单的编码或加密。对于公交卡数据结构通常更复杂涉及交易记录、余额、密钥版本号等。直接模拟一张完整的、可用的公交卡几乎是不可能的因为后台系统有复杂的双向认证和交易流水校验机制。然而我们可以研究其NDEFNFC Data Exchange Format数据。很多新型的公交卡或智能海报会使用NDEF格式来存储一个指向充值网页的URI。下面是一个创建并写入NDEF格式URI记录的示例private fun writeNdefUriToTag(tag: Tag, uriString: String) { val ndef Ndef.get(tag) ndef?.let { try { it.connect() if (it.isWritable) { val uriRecord NdefRecord.createUri(uriString) val ndefMessage NdefMessage(arrayOf(uriRecord)) it.writeNdefMessage(ndefMessage) log(成功写入NDEF URI: $uriString) } else { log(标签不可写) } it.close() } catch (e: IOException) { log(写入NDEF时发生IO异常: ${e.message}) } catch (e: FormatException) { log(NDEF格式异常: ${e.message}) } } }这个函数会将一个URI比如https://www.example.com/recharge写入到标签的NDEF区域。当用户用手机贴近这张卡时手机会自动识别并提示打开这个链接。6. 性能优化与异常处理实战经验在真实项目开发中稳定性至关重要。NFC通信容易受到干扰卡片移开过快、信号弱都会导致操作失败。因此完善的异常处理和用户反馈机制是必须的。连接超时与重试mfc.connect()可以设置超时时间。对于关键操作可以封装一个带重试逻辑的连接方法。优雅的断开任何操作结束后务必调用mfc.close()。最好在finally块中执行确保资源释放。用户界面反馈在尝试认证、读写时通过振动、声音或界面文字实时反馈当前状态。例如认证成功时短振动一次读写失败时长振动一次。日志记录将关键操作和异常信息记录到日志文件或控制台便于后期排查问题。可以区分不同级别如DEBUG,INFO,ERROR。这里分享一个我遇到过的坑在快速连续刷卡时有时会收到TagLostException。这是因为上一张卡片的连接还未完全释放新的卡片就靠近了。解决方案是在onNewIntent中处理新标签前先检查并关闭上一个可能存在的连接并加入一个短暂的状态锁防止并发操作。private var isProcessingTag false private var currentTag: Tag? null private fun processIntent(intent: Intent) { if (isProcessingTag) { log(正在处理上一张卡片请稍候...) return } val tag intent.getParcelableExtraTag(NfcAdapter.EXTRA_TAG) tag?.let { isProcessingTag true currentTag it // 在子线程中处理避免阻塞UI Thread { handleTag(it) isProcessingTag false currentTag null }.start() } }开发过程中使用一张UID可写的空白M1卡或UID卡进行测试是极其重要的。你可以在上面反复练习读写、修改密钥而不用担心损坏重要的原始卡片。