TalerNfcService.kt (15218B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2024 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.lib.android 18 19 import android.app.Activity 20 import android.content.BroadcastReceiver 21 import android.content.ComponentName 22 import android.content.Context 23 import android.content.Intent 24 import android.content.IntentFilter 25 import android.nfc.NdefMessage 26 import android.nfc.NdefRecord 27 import android.nfc.NfcAdapter.getDefaultAdapter 28 import android.nfc.cardemulation.CardEmulation 29 import android.nfc.cardemulation.HostApduService 30 import android.os.Bundle 31 import android.util.Log 32 import androidx.core.content.ContextCompat 33 import androidx.core.content.IntentCompat 34 import java.math.BigInteger 35 36 class TalerNfcService : HostApduService() { 37 private var ndefMessage: NdefMessage? = null 38 39 private val ndefUriBytes: ByteArray? 40 get() = ndefMessage?.toByteArray() 41 42 private val ndefUriLen: ByteArray? 43 get() = ndefUriBytes?.size?.toLong()?.let { size -> 44 fillByteArrayToFixedDimension( 45 BigInteger.valueOf(size).toByteArray(), 46 2 47 ) 48 } 49 50 private var readCapabilityContainerCheck = false 51 52 private val broadcastReceiver = object: BroadcastReceiver() { 53 override fun onReceive(context: Context?, intent: Intent?) { 54 when (intent?.action) { 55 SET_URI_INTENT -> intent.getStringExtra("uri")?.let { uri -> 56 Log.d(TAG, "onReceive(SET_URI_INTENT) | URI: $uri") 57 ndefMessage = NdefMessage(createUriRecord(uri)) 58 } 59 60 SET_NDEF_INTENT -> IntentCompat.getParcelableExtra( 61 intent, 62 "ndef", 63 NdefMessage::class.java, 64 )?.let { ndef -> 65 Log.d(TAG, "onReceive(SET_NDEF_INTENT) | NDEF: $ndef") 66 ndefMessage = ndef 67 } 68 69 CLEAR_NDEF_INTENT -> { 70 Log.d(TAG, "onReceive(CLEAR_NDEF_INTENT)") 71 ndefMessage = null 72 } 73 } 74 } 75 } 76 77 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 78 return START_STICKY 79 } 80 81 override fun processCommandApdu( 82 commandApdu: ByteArray?, 83 extras: Bundle? 84 ): ByteArray { 85 86 Log.d(TAG, "Processing command APDU") 87 88 if (commandApdu == null) { 89 Log.d(TAG, "processCommandApi() no data received") 90 return A_ERROR 91 } 92 93 val message = ndefMessage 94 if (message == null) { 95 Log.d(TAG, "processCommandApi() no data to write") 96 return A_ERROR 97 } 98 99 // 100 // The following flow is based on Appendix E "Example of Mapping Version 2.0 Command Flow" 101 // in the NFC Forum specification 102 // 103 Log.d(TAG, "processCommandApdu() | incoming commandApdu: " + commandApdu.toHex()) 104 105 // 106 // First command: NDEF Tag Application select (Section 5.5.2 in NFC Forum spec) 107 // 108 if (APDU_SELECT.contentEquals(commandApdu)) { 109 Log.d(TAG, "APDU_SELECT triggered. Our Response: " + A_OKAY.toHex()) 110 return A_OKAY 111 } 112 113 // 114 // Second command: Capability Container select (Section 5.5.3 in NFC Forum spec) 115 // 116 if (CAPABILITY_CONTAINER_OK.contentEquals(commandApdu)) { 117 Log.d(TAG, "CAPABILITY_CONTAINER_OK triggered. Our Response: " + A_OKAY.toHex()) 118 return A_OKAY 119 } 120 121 // 122 // Third command: ReadBinary data from CC file (Section 5.5.4 in NFC Forum spec) 123 // 124 if (READ_CAPABILITY_CONTAINER.contentEquals(commandApdu) && !readCapabilityContainerCheck) { 125 Log.d(TAG, "READ_CAPABILITY_CONTAINER triggered. Our Response: " + READ_CAPABILITY_CONTAINER_RESPONSE.toHex()) 126 127 readCapabilityContainerCheck = true 128 return READ_CAPABILITY_CONTAINER_RESPONSE 129 } 130 131 // 132 // Fourth command: NDEF Select command (Section 5.5.5 in NFC Forum spec) 133 // 134 if (NDEF_SELECT_OK.contentEquals(commandApdu)) { 135 Log.d(TAG, "NDEF_SELECT_OK triggered. Our Response: " + A_OKAY.toHex()) 136 return A_OKAY 137 } 138 139 if (NDEF_READ_BINARY_NLEN.contentEquals(commandApdu)) { 140 // Build our response 141 val response = ByteArray(ndefUriLen!!.size + A_OKAY.size) 142 System.arraycopy(ndefUriLen!!, 0, response, 0, ndefUriLen!!.size) 143 System.arraycopy(A_OKAY, 0, response, ndefUriLen!!.size, A_OKAY.size) 144 145 Log.d(TAG, "NDEF_READ_BINARY_NLEN triggered. Our Response: " + response.toHex()) 146 147 readCapabilityContainerCheck = false 148 return response 149 } 150 151 if (commandApdu.sliceArray(0..1).contentEquals(NDEF_READ_BINARY)) { 152 val offset = commandApdu.sliceArray(2..3).toHex().toInt(16) 153 val length = commandApdu.sliceArray(4..4).toHex().toInt(16) 154 155 val fullResponse = ByteArray(ndefUriLen!!.size + ndefUriBytes!!.size) 156 System.arraycopy(ndefUriLen!!, 0, fullResponse, 0, ndefUriLen!!.size) 157 System.arraycopy( 158 ndefUriBytes!!, 159 0, 160 fullResponse, 161 ndefUriLen!!.size, 162 ndefUriBytes!!.size, 163 ) 164 165 Log.d(TAG, "NDEF_READ_BINARY triggered. Full data: " + fullResponse.toHex()) 166 Log.d(TAG, "READ_BINARY - OFFSET: $offset - LEN: $length") 167 168 val slicedResponse = fullResponse.sliceArray(offset until fullResponse.size) 169 170 // Build our response 171 val realLength = if (slicedResponse.size <= length) slicedResponse.size else length 172 val response = ByteArray(realLength + A_OKAY.size) 173 174 System.arraycopy(slicedResponse, 0, response, 0, realLength) 175 System.arraycopy(A_OKAY, 0, response, realLength, A_OKAY.size) 176 177 Log.d(TAG, "NDEF_READ_BINARY triggered. Our Response: " + response.toHex()) 178 179 readCapabilityContainerCheck = false 180 return response 181 } 182 183 // 184 // We're doing something outside our scope 185 // 186 Log.wtf(TAG, "processCommandApdu() | I don't know what's going on!!!") 187 return A_ERROR 188 } 189 190 override fun onDeactivated(reason: Int) { 191 Log.d(TAG, "onDeactivated() Fired! Reason: $reason") 192 } 193 194 private fun ByteArray.toHex(): String { 195 val result = StringBuffer() 196 197 forEach { 198 val octet = it.toInt() 199 val firstIndex = (octet and 0xF0).ushr(4) 200 val secondIndex = octet and 0x0F 201 result.append(HEX_CHARS[firstIndex]) 202 result.append(HEX_CHARS[secondIndex]) 203 } 204 205 return result.toString() 206 } 207 208 private fun createUriRecord(uri: String) = NdefRecord.createUri(uri) 209 210 private fun fillByteArrayToFixedDimension(array: ByteArray, fixedSize: Int): ByteArray { 211 if (array.size == fixedSize) { 212 return array 213 } 214 215 val start = byteArrayOf(0x00.toByte()) 216 val filledArray = ByteArray(start.size + array.size) 217 System.arraycopy(start, 0, filledArray, 0, start.size) 218 System.arraycopy(array, 0, filledArray, start.size, array.size) 219 return fillByteArrayToFixedDimension(filledArray, fixedSize) 220 } 221 222 override fun onCreate() { 223 super.onCreate() 224 Log.d(TAG, "onCreate() service running") 225 val intentFilter = IntentFilter() 226 intentFilter.addAction(SET_URI_INTENT) 227 intentFilter.addAction(SET_NDEF_INTENT) 228 intentFilter.addAction(CLEAR_NDEF_INTENT) 229 ContextCompat.registerReceiver( 230 this@TalerNfcService, 231 broadcastReceiver, 232 intentFilter, 233 ContextCompat.RECEIVER_NOT_EXPORTED, 234 ) 235 } 236 237 override fun onDestroy() { 238 super.onDestroy() 239 Log.d(TAG, "onDestroy() NFC service") 240 unregisterReceiver(broadcastReceiver) 241 ndefMessage = null 242 } 243 244 companion object { 245 private const val TAG = "taler-wallet-hce" 246 const val SET_URI_INTENT = "taler-wallet-set-url" 247 const val SET_NDEF_INTENT = "taler-wallet-set-ndef" 248 const val CLEAR_NDEF_INTENT = "taler-wallet-clear-ndef" 249 250 private val APDU_SELECT = byteArrayOf( 251 0x00.toByte(), // CLA - Class - Class of instruction 252 0xA4.toByte(), // INS - Instruction - Instruction code 253 0x04.toByte(), // P1 - Parameter 1 - Instruction parameter 1 254 0x00.toByte(), // P2 - Parameter 2 - Instruction parameter 2 255 0x07.toByte(), // Lc field - Number of bytes present in the data field of the command 256 0xD2.toByte(), 257 0x76.toByte(), 258 0x00.toByte(), 259 0x00.toByte(), 260 0x85.toByte(), 261 0x01.toByte(), 262 0x01.toByte(), // NDEF Tag Application name 263 0x00.toByte(), // Le field - Maximum number of bytes expected in the data field of the response to the command 264 ) 265 266 private val CAPABILITY_CONTAINER_OK = byteArrayOf( 267 0x00.toByte(), // CLA - Class - Class of instruction 268 0xa4.toByte(), // INS - Instruction - Instruction code 269 0x00.toByte(), // P1 - Parameter 1 - Instruction parameter 1 270 0x0c.toByte(), // P2 - Parameter 2 - Instruction parameter 2 271 0x02.toByte(), // Lc field - Number of bytes present in the data field of the command 272 0xe1.toByte(), 273 0x03.toByte(), // file identifier of the CC file 274 ) 275 276 private val READ_CAPABILITY_CONTAINER = byteArrayOf( 277 0x00.toByte(), // CLA - Class - Class of instruction 278 0xb0.toByte(), // INS - Instruction - Instruction code 279 0x00.toByte(), // P1 - Parameter 1 - Instruction parameter 1 280 0x00.toByte(), // P2 - Parameter 2 - Instruction parameter 2 281 0x0f.toByte(), // Lc field - Number of bytes present in the data field of the command 282 ) 283 284 private val READ_CAPABILITY_CONTAINER_RESPONSE = byteArrayOf( 285 0x00.toByte(), 0x0F.toByte(), // CCLEN length of the CC file 286 0x20.toByte(), // Mapping Version 2.0 287 0x00.toByte(), 0x3B.toByte(), // MLe maximum 288 0x00.toByte(), 0x34.toByte(), // MLc maximum 289 0x04.toByte(), // T field of the NDEF File Control TLV 290 0x06.toByte(), // L field of the NDEF File Control TLV 291 0xE1.toByte(), 0x04.toByte(), // File Identifier of NDEF file 292 0x00.toByte(), 0xFE.toByte(), // Maximum NDEF file size of 65534 bytes 293 0x00.toByte(), // Read access without any security 294 0xFF.toByte(), // Write access without any security 295 0x90.toByte(), 0x00.toByte(), // A_OKAY 296 ) 297 298 private val NDEF_SELECT_OK = byteArrayOf( 299 0x00.toByte(), // CLA - Class - Class of instruction 300 0xa4.toByte(), // Instruction byte (INS) for Select command 301 0x00.toByte(), // Parameter byte (P1), select by identifier 302 0x0c.toByte(), // Parameter byte (P1), select by identifier 303 0x02.toByte(), // Lc field - Number of bytes present in the data field of the command 304 0xE1.toByte(), 305 0x04.toByte(), // file identifier of the NDEF file retrieved from the CC file 306 ) 307 308 private val NDEF_READ_BINARY = byteArrayOf( 309 0x00.toByte(), // Class byte (CLA) 310 0xb0.toByte(), // Instruction byte (INS) for ReadBinary command 311 ) 312 313 private val NDEF_READ_BINARY_NLEN = byteArrayOf( 314 0x00.toByte(), // Class byte (CLA) 315 0xb0.toByte(), // Instruction byte (INS) for ReadBinary command 316 0x00.toByte(), 317 0x00.toByte(), // Parameter byte (P1, P2), offset inside the CC file 318 0x02.toByte(), // Le field 319 ) 320 321 private val A_OKAY = byteArrayOf( 322 0x90.toByte(), // SW1 Status byte 1 - Command processing status 323 0x00.toByte(), // SW2 Status byte 2 - Command processing qualifier 324 ) 325 326 private val A_ERROR = byteArrayOf( 327 0x6A.toByte(), // SW1 Status byte 1 - Command processing status 328 0x82.toByte(), // SW2 Status byte 2 - Command processing qualifier 329 ) 330 331 private val HEX_CHARS = "0123456789ABCDEF".toCharArray() 332 333 /** 334 * Returns true if NFC is supported and false otherwise. 335 */ 336 fun hasNfc(context: Context): Boolean { 337 return getDefaultAdapter(context) != null 338 } 339 340 fun setDefaultHandler(activity: Activity) { 341 val adapter = getDefaultAdapter(activity) ?: return 342 val emulation = CardEmulation.getInstance(adapter) 343 // TODO: find an alternative for when canonicalName is null 344 try { 345 val cn = ComponentName(activity, TalerNfcService::class.java) 346 emulation.setPreferredService(activity, cn) 347 } catch (e: NullPointerException) { 348 Log.d(TAG, "Not setting this app as the preferred NFC handler!") 349 } 350 } 351 352 fun unsetDefaultHandler(activity: Activity) { 353 val adapter = getDefaultAdapter(activity) ?: return 354 val emulation = CardEmulation.getInstance(adapter) 355 emulation.unsetPreferredService(activity) 356 } 357 358 fun startService(activity: Activity) { 359 val intent = Intent(activity, TalerNfcService::class.java) 360 activity.startService(intent) 361 } 362 363 fun stopService(activity: Activity) { 364 val intent = Intent(activity, TalerNfcService::class.java) 365 activity.stopService(intent) 366 } 367 368 fun setUri(activity: Activity, uri: String) { 369 if (!hasNfc(activity)) return 370 val intent = Intent(SET_URI_INTENT) 371 intent.setPackage(activity.packageName) 372 intent.putExtra("uri", uri) 373 activity.sendBroadcast(intent) 374 } 375 376 fun setNdefPayload(activity: Activity, ndef: NdefMessage) { 377 if (!hasNfc(activity)) return 378 val intent = Intent(SET_NDEF_INTENT) 379 intent.setPackage(activity.packageName) 380 intent.putExtra("ndef", ndef) 381 activity.sendBroadcast(intent) 382 } 383 384 fun clearNdefPayload(activity: Activity) { 385 if (!hasNfc(activity)) return 386 val intent = Intent(CLEAR_NDEF_INTENT) 387 intent.setPackage(activity.packageName) 388 activity.sendBroadcast(intent) 389 } 390 } 391 }