summaryrefslogtreecommitdiff
path: root/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt
blob: 09c1470856e532a3b144e0b7a6d1b897b0deb9e0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/*
 * This file is part of GNU Taler
 * (C) 2020 Taler Systems S.A.
 *
 * GNU Taler is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3, or (at your option) any later version.
 *
 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

package net.taler.merchantpos

import android.app.Activity
import android.content.Context
import android.nfc.NfcAdapter
import android.nfc.NfcAdapter.FLAG_READER_NFC_A
import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.util.Log
import net.taler.merchantpos.Utils.hexStringToByteArray
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import java.net.URL
import javax.net.ssl.HttpsURLConnection

@Suppress("unused")
private const val TALER_AID = "A0000002471001"

class NfcManager : NfcAdapter.ReaderCallback {

    companion object {
        const val TAG = "taler-merchant"

        /**
         * Returns true if NFC is supported and false otherwise.
         */
        fun hasNfc(context: Context): Boolean {
            return getNfcAdapter(context) != null
        }

        /**
         * Enables NFC reader mode. Don't forget to call [stop] afterwards.
         */
        fun start(activity: Activity, nfcManager: NfcManager) {
            getNfcAdapter(activity)?.enableReaderMode(activity, nfcManager, nfcManager.flags, null)
        }

        /**
         * Disables NFC reader mode. Call after [start].
         */
        fun stop(activity: Activity) {
            getNfcAdapter(activity)?.disableReaderMode(activity)
        }

        private fun getNfcAdapter(context: Context): NfcAdapter? {
            return NfcAdapter.getDefaultAdapter(context)
        }
    }

    private val flags = FLAG_READER_NFC_A or FLAG_READER_SKIP_NDEF_CHECK

    private var tagString: String? = null
    private var currentTag: IsoDep? = null

    fun setTagString(tagString: String) {
        this.tagString = tagString
    }

    override fun onTagDiscovered(tag: Tag?) {

        Log.v(TAG, "tag discovered")

        val isoDep = IsoDep.get(tag)
        isoDep.connect()

        currentTag = isoDep

        isoDep.transceive(apduSelectFile())

        val tagString: String? = tagString
        if (tagString != null) {
            isoDep.transceive(apduPutTalerData(1, tagString.toByteArray()))
        }

        // FIXME: use better pattern for sleeps in between requests
        // -> start with fast polling, poll more slowly if no requests are coming

        while (true) {
            try {
                val reqFrame = isoDep.transceive(apduGetData())
                if (reqFrame.size < 2) {
                    Log.v(TAG, "request frame too small")
                    break
                }
                val req = ByteArray(reqFrame.size - 2)
                if (req.isEmpty()) {
                    continue
                }
                reqFrame.copyInto(req, 0, 0, reqFrame.size - 2)
                val jsonReq = JSONObject(req.toString(Charsets.UTF_8))
                val reqId = jsonReq.getInt("id")
                Log.v(TAG, "got request $jsonReq")
                val jsonInnerReq = jsonReq.getJSONObject("request")
                val method = jsonInnerReq.getString("method")
                val urlStr = jsonInnerReq.getString("url")
                Log.v(TAG, "url '$urlStr'")
                Log.v(TAG, "method '$method'")
                val url = URL(urlStr)
                val conn: HttpsURLConnection = url.openConnection() as HttpsURLConnection
                conn.setRequestProperty("Accept", "application/json")
                conn.connectTimeout = 5000
                conn.doInput = true
                when (method) {
                    "get" -> {
                        conn.requestMethod = "GET"
                    }
                    "postJson" -> {
                        conn.requestMethod = "POST"
                        conn.doOutput = true
                        conn.setRequestProperty("Content-Type", "application/json; utf-8")
                        val body = jsonInnerReq.getString("body")
                        conn.outputStream.write(body.toByteArray(Charsets.UTF_8))
                    }
                    else -> {
                        throw Exception("method not supported")
                    }
                }
                Log.v(TAG, "connecting")
                conn.connect()
                Log.v(TAG, "connected")

                val statusCode = conn.responseCode
                val tunnelResp = JSONObject()
                tunnelResp.put("id", reqId)
                tunnelResp.put("status", conn.responseCode)

                if (statusCode == 200) {
                    val stream = conn.inputStream
                    val httpResp = stream.buffered().readBytes()
                    tunnelResp.put("responseJson", JSONObject(httpResp.toString(Charsets.UTF_8)))
                }

                Log.v(TAG, "sending: $tunnelResp")

                isoDep.transceive(apduPutTalerData(2, tunnelResp.toString().toByteArray()))
            } catch (e: Exception) {
                Log.v(TAG, "exception during NFC loop: $e")
                break
            }
        }

        isoDep.close()
    }

    private fun writeApduLength(stream: ByteArrayOutputStream, size: Int) {
        when {
            size == 0 -> {
                // No size field needed!
            }
            size <= 255 -> // One byte size field
                stream.write(size)
            size <= 65535 -> {
                stream.write(0)
                // FIXME: is this supposed to be little or big endian?
                stream.write(size and 0xFF)
                stream.write((size ushr 8) and 0xFF)
            }
            else -> throw Error("payload too big")
        }
    }

    private fun apduSelectFile(): ByteArray {
        return hexStringToByteArray("00A4040007A0000002471001")
    }

    private fun apduPutData(payload: ByteArray): ByteArray {
        val stream = ByteArrayOutputStream()

        // Class
        stream.write(0x00)

        // Instruction 0xDA = put data
        stream.write(0xDA)

        // Instruction parameters
        // (proprietary encoding)
        stream.write(0x01)
        stream.write(0x00)

        writeApduLength(stream, payload.size)

        stream.write(payload)

        return stream.toByteArray()
    }

    private fun apduPutTalerData(talerInst: Int, payload: ByteArray): ByteArray {
        val realPayload = ByteArrayOutputStream()
        realPayload.write(talerInst)
        realPayload.write(payload)
        return apduPutData(realPayload.toByteArray())
    }

    private fun apduGetData(): ByteArray {
        val stream = ByteArrayOutputStream()

        // Class
        stream.write(0x00)

        // Instruction 0xCA = get data
        stream.write(0xCA)

        // Instruction parameters
        // (proprietary encoding)
        stream.write(0x01)
        stream.write(0x00)

        // Max expected response size, two
        // zero bytes denotes 65536
        stream.write(0x0)
        stream.write(0x0)

        return stream.toByteArray()
    }

}