1
+ package com.qifan.nfcbank.cardEmulation
2
+
3
+ import android.nfc.cardemulation.HostApduService
4
+ import android.app.Service
5
+ import android.content.Intent
6
+ import android.nfc.NdefMessage
7
+ import android.os.Bundle
8
+ import android.util.Log
9
+ import java.util.*
10
+ import android.nfc.NdefRecord
11
+ import android.widget.Toast
12
+ import java.io.UnsupportedEncodingException
13
+ import java.math.BigInteger
14
+
15
+
16
+ /* *
17
+ * Created by Qifan on 05/12/2018.
18
+ */
19
+
20
+ class KHostApduService : HostApduService () {
21
+
22
+ private val TAG = " HostApduService"
23
+
24
+ private val APDU_SELECT = byteArrayOf(
25
+ 0x00 .toByte(), // CLA - Class - Class of instruction
26
+ 0xA4 .toByte(), // INS - Instruction - Instruction code
27
+ 0x04 .toByte(), // P1 - Parameter 1 - Instruction parameter 1
28
+ 0x00 .toByte(), // P2 - Parameter 2 - Instruction parameter 2
29
+ 0x07 .toByte(), // Lc field - Number of bytes present in the data field of the command
30
+ 0xD2 .toByte(), 0x76 .toByte(), 0x00 .toByte(), 0x00 .toByte(), 0x85 .toByte(), 0x01 .toByte(), 0x01 .toByte(), // NDEF Tag Application name
31
+ 0x00 .toByte() // Le field - Maximum number of bytes expected in the data field of the response to the command
32
+ )
33
+
34
+ private val CAPABILITY_CONTAINER_OK = byteArrayOf(
35
+ 0x00 .toByte(), // CLA - Class - Class of instruction
36
+ 0xa4 .toByte(), // INS - Instruction - Instruction code
37
+ 0x00 .toByte(), // P1 - Parameter 1 - Instruction parameter 1
38
+ 0x0c .toByte(), // P2 - Parameter 2 - Instruction parameter 2
39
+ 0x02 .toByte(), // Lc field - Number of bytes present in the data field of the command
40
+ 0xe1 .toByte(), 0x03 .toByte() // file identifier of the CC file
41
+ )
42
+
43
+ private val READ_CAPABILITY_CONTAINER = byteArrayOf(
44
+ 0x00 .toByte(), // CLA - Class - Class of instruction
45
+ 0xb0 .toByte(), // INS - Instruction - Instruction code
46
+ 0x00 .toByte(), // P1 - Parameter 1 - Instruction parameter 1
47
+ 0x00 .toByte(), // P2 - Parameter 2 - Instruction parameter 2
48
+ 0x0f .toByte() // Lc field - Number of bytes present in the data field of the command
49
+ )
50
+
51
+ // In the scenario that we have done a CC read, the same byte[] match
52
+ // for ReadBinary would trigger and we don't want that in succession
53
+ private var READ_CAPABILITY_CONTAINER_CHECK = false
54
+
55
+ private val READ_CAPABILITY_CONTAINER_RESPONSE = byteArrayOf(
56
+ 0x00 .toByte(), 0x11 .toByte(), // CCLEN length of the CC file
57
+ 0x20 .toByte(), // Mapping Version 2.0
58
+ 0xFF .toByte(), 0xFF .toByte(), // MLe maximum
59
+ 0xFF .toByte(), 0xFF .toByte(), // MLc maximum
60
+ 0x04 .toByte(), // T field of the NDEF File Control TLV
61
+ 0x06 .toByte(), // L field of the NDEF File Control TLV
62
+ 0xE1 .toByte(), 0x04 .toByte(), // File Identifier of NDEF file
63
+ 0xFF .toByte(), 0xFE .toByte(), // Maximum NDEF file size of 65534 bytes
64
+ 0x00 .toByte(), // Read access without any security
65
+ 0xFF .toByte(), // Write access without any security
66
+ 0x90 .toByte(), 0x00 .toByte() // A_OKAY
67
+ )
68
+
69
+ private val NDEF_SELECT_OK = byteArrayOf(
70
+ 0x00 .toByte(), // CLA - Class - Class of instruction
71
+ 0xa4 .toByte(), // Instruction byte (INS) for Select command
72
+ 0x00 .toByte(), // Parameter byte (P1), select by identifier
73
+ 0x0c .toByte(), // Parameter byte (P1), select by identifier
74
+ 0x02 .toByte(), // Lc field - Number of bytes present in the data field of the command
75
+ 0xE1 .toByte(), 0x04 .toByte() // file identifier of the NDEF file retrieved from the CC file
76
+ )
77
+
78
+ private val NDEF_READ_BINARY = byteArrayOf(
79
+ 0x00 .toByte(), // Class byte (CLA)
80
+ 0xb0 .toByte() // Instruction byte (INS) for ReadBinary command
81
+ )
82
+
83
+ private val NDEF_READ_BINARY_NLEN = byteArrayOf(
84
+ 0x00 .toByte(), // Class byte (CLA)
85
+ 0xb0 .toByte(), // Instruction byte (INS) for ReadBinary command
86
+ 0x00 .toByte(), 0x00 .toByte(), // Parameter byte (P1, P2), offset inside the CC file
87
+ 0x02 .toByte() // Le field
88
+ )
89
+
90
+ private val A_OKAY = byteArrayOf(
91
+ 0x90 .toByte(), // SW1 Status byte 1 - Command processing status
92
+ 0x00 .toByte() // SW2 Status byte 2 - Command processing qualifier
93
+ )
94
+
95
+ private val A_ERROR = byteArrayOf(
96
+ 0x6A .toByte(), // SW1 Status byte 1 - Command processing status
97
+ 0x82 .toByte() // SW2 Status byte 2 - Command processing qualifier
98
+ )
99
+
100
+ private val NDEF_ID = byteArrayOf(0xE1 .toByte(), 0x04 .toByte())
101
+
102
+ private var NDEF_URI = NdefMessage (createTextRecord(" " , " Hello world" , NDEF_ID ))
103
+ private var NDEF_URI_BYTES = NDEF_URI .toByteArray()
104
+ private var NDEF_URI_LEN = fillByteArrayToFixedDimension(BigInteger .valueOf(NDEF_URI_BYTES .size.toLong()).toByteArray(), 2 )
105
+
106
+
107
+
108
+ override fun onStartCommand (intent : Intent , flags : Int , startId : Int ): Int {
109
+
110
+ if (intent.hasExtra(" ndefMessage" )) {
111
+ NDEF_URI = NdefMessage (createTextRecord(" " , intent.getStringExtra(" ndefMessage" ), NDEF_ID ))
112
+
113
+ NDEF_URI_BYTES = NDEF_URI .toByteArray()
114
+ NDEF_URI_LEN = fillByteArrayToFixedDimension(BigInteger .valueOf(NDEF_URI_BYTES .size.toLong()).toByteArray(), 2 )
115
+
116
+ Toast .makeText(applicationContext," message has been sent" ,Toast .LENGTH_LONG ).show()
117
+ }
118
+
119
+ Log .i(TAG , " onStartCommand() | NDEF" + NDEF_URI .toString())
120
+
121
+ return Service .START_STICKY
122
+ }
123
+
124
+ override fun processCommandApdu (commandApdu : ByteArray , extras : Bundle ? ): ByteArray {
125
+
126
+ //
127
+ // The following flow is based on Appendix E "Example of Mapping Version 2.0 Command Flow"
128
+ // in the NFC Forum specification
129
+ //
130
+ Log .i(TAG , " processCommandApdu() | incoming commandApdu: " + commandApdu.toHex())
131
+
132
+ //
133
+ // First command: NDEF Tag Application select (Section 5.5.2 in NFC Forum spec)
134
+ //
135
+ if (Arrays .equals(APDU_SELECT , commandApdu)) {
136
+ Log .i(TAG , " APDU_SELECT triggered. Our Response: " + A_OKAY .toHex())
137
+ return A_OKAY
138
+ }
139
+
140
+ //
141
+ // Second command: Capability Container select (Section 5.5.3 in NFC Forum spec)
142
+ //
143
+ if (Arrays .equals(CAPABILITY_CONTAINER_OK , commandApdu)) {
144
+ Log .i(TAG , " CAPABILITY_CONTAINER_OK triggered. Our Response: " + A_OKAY .toHex())
145
+ return A_OKAY
146
+ }
147
+
148
+ //
149
+ // Third command: ReadBinary data from CC file (Section 5.5.4 in NFC Forum spec)
150
+ //
151
+ if (Arrays .equals(READ_CAPABILITY_CONTAINER , commandApdu) && ! READ_CAPABILITY_CONTAINER_CHECK ) {
152
+ Log .i(TAG , " READ_CAPABILITY_CONTAINER triggered. Our Response: " + READ_CAPABILITY_CONTAINER_RESPONSE .toHex())
153
+ READ_CAPABILITY_CONTAINER_CHECK = true
154
+ return READ_CAPABILITY_CONTAINER_RESPONSE
155
+ }
156
+
157
+ //
158
+ // Fourth command: NDEF Select command (Section 5.5.5 in NFC Forum spec)
159
+ //
160
+ if (Arrays .equals(NDEF_SELECT_OK , commandApdu)) {
161
+ Log .i(TAG , " NDEF_SELECT_OK triggered. Our Response: " + A_OKAY .toHex())
162
+ return A_OKAY
163
+ }
164
+
165
+ if (Arrays .equals(NDEF_READ_BINARY_NLEN , commandApdu)) {
166
+ // Build our response
167
+ val response = ByteArray (NDEF_URI_LEN .size + A_OKAY .size)
168
+ System .arraycopy(NDEF_URI_LEN , 0 , response, 0 , NDEF_URI_LEN .size)
169
+ System .arraycopy(A_OKAY , 0 , response, NDEF_URI_LEN .size, A_OKAY .size)
170
+
171
+ Log .i(TAG , " NDEF_READ_BINARY_NLEN triggered. Our Response: " + response.toHex())
172
+
173
+ READ_CAPABILITY_CONTAINER_CHECK = false
174
+ return response
175
+ }
176
+
177
+ if (Arrays .equals(commandApdu.sliceArray(0 .. 1 ), NDEF_READ_BINARY )) {
178
+ val offset = commandApdu.sliceArray(2 .. 3 ).toHex().toInt(16 )
179
+ val length = commandApdu.sliceArray(4 .. 4 ).toHex().toInt(16 )
180
+
181
+ val fullResponse = ByteArray (NDEF_URI_LEN .size + NDEF_URI_BYTES .size)
182
+ System .arraycopy(NDEF_URI_LEN , 0 , fullResponse, 0 , NDEF_URI_LEN .size)
183
+ System .arraycopy(NDEF_URI_BYTES , 0 , fullResponse, NDEF_URI_LEN .size, NDEF_URI_BYTES .size)
184
+
185
+ Log .i(TAG , " NDEF_READ_BINARY triggered. Full data: " + fullResponse.toHex())
186
+ Log .i(TAG , " READ_BINARY - OFFSET: " + offset + " - LEN: " + length)
187
+
188
+ val slicedResponse = fullResponse.sliceArray(offset.. fullResponse.size)
189
+
190
+ // Build our response
191
+ val realLength = if (slicedResponse.size <= length) slicedResponse.size else length
192
+ val response = ByteArray (realLength + A_OKAY .size)
193
+
194
+ System .arraycopy(slicedResponse, 0 , response, 0 , realLength)
195
+ System .arraycopy(A_OKAY , 0 , response, realLength, A_OKAY .size)
196
+
197
+ Log .i(TAG , " NDEF_READ_BINARY triggered. Our Response: " + response.toHex())
198
+
199
+ READ_CAPABILITY_CONTAINER_CHECK = false
200
+ return response
201
+ }
202
+
203
+ //
204
+ // We're doing something outside our scope
205
+ //
206
+ Log .wtf(TAG , " processCommandApdu() | I don't know what's going on!!!" )
207
+ return A_ERROR
208
+ }
209
+
210
+ override fun onDeactivated (reason : Int ) {
211
+ Log .i(TAG , " onDeactivated() Fired! Reason: $reason " )
212
+ }
213
+
214
+
215
+
216
+ private val HEX_CHARS = " 0123456789ABCDEF" .toCharArray()
217
+
218
+ fun ByteArray.toHex () : String {
219
+ val result = StringBuffer ()
220
+
221
+ forEach {
222
+ val octet = it.toInt()
223
+ val firstIndex = (octet and 0xF0 ).ushr(4 )
224
+ val secondIndex = octet and 0x0F
225
+ result.append(HEX_CHARS [firstIndex])
226
+ result.append(HEX_CHARS [secondIndex])
227
+ }
228
+
229
+ return result.toString()
230
+ }
231
+
232
+ fun String.hexStringToByteArray () : ByteArray {
233
+
234
+ val result = ByteArray (length / 2 )
235
+
236
+ for (i in 0 until length step 2 ) {
237
+ val firstIndex = HEX_CHARS .indexOf(this [i]);
238
+ val secondIndex = HEX_CHARS .indexOf(this [i + 1 ]);
239
+
240
+ val octet = firstIndex.shl(4 ).or (secondIndex)
241
+ result.set(i.shr(1 ), octet.toByte())
242
+ }
243
+
244
+ return result
245
+ }
246
+
247
+ fun createTextRecord (language : String , text : String , id : ByteArray ): NdefRecord {
248
+ val languageBytes: ByteArray
249
+ val textBytes: ByteArray
250
+ try {
251
+ languageBytes = language.toByteArray(charset(" US-ASCII" ))
252
+ textBytes = text.toByteArray(charset(" UTF-8" ))
253
+ } catch (e: UnsupportedEncodingException ) {
254
+ throw AssertionError (e)
255
+ }
256
+
257
+ val recordPayload = ByteArray (1 + (languageBytes.size and 0x03F ) + textBytes.size)
258
+
259
+ recordPayload[0 ] = (languageBytes.size and 0x03F ).toByte()
260
+ System .arraycopy(languageBytes, 0 , recordPayload, 1 , languageBytes.size and 0x03F )
261
+ System .arraycopy(textBytes, 0 , recordPayload, 1 + (languageBytes.size and 0x03F ), textBytes.size)
262
+
263
+ return NdefRecord (NdefRecord .TNF_WELL_KNOWN , NdefRecord .RTD_TEXT , id, recordPayload)
264
+ }
265
+
266
+ private fun fillByteArrayToFixedDimension (array : ByteArray , fixedSize : Int ): ByteArray {
267
+ if (array.size == fixedSize) {
268
+ return array
269
+ }
270
+
271
+ val start = byteArrayOf(0x00 .toByte())
272
+ val filledArray = ByteArray (start.size + array.size)
273
+ System .arraycopy(start, 0 , filledArray, 0 , start.size)
274
+ System .arraycopy(array, 0 , filledArray, start.size, array.size)
275
+ return fillByteArrayToFixedDimension(filledArray, fixedSize)
276
+ }
277
+ }
0 commit comments