taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

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 }