1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.nfc.handover;
18 
19 import java.nio.BufferUnderflowException;
20 import java.nio.ByteBuffer;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayList;
23 import java.nio.charset.Charset;
24 import java.util.Arrays;
25 import java.util.Random;
26 
27 import android.bluetooth.BluetoothAdapter;
28 import android.bluetooth.BluetoothDevice;
29 import android.bluetooth.OobData;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.nfc.FormatException;
33 import android.nfc.NdefMessage;
34 import android.nfc.NdefRecord;
35 import android.os.UserHandle;
36 import android.util.Log;
37 
38 /**
39  * Manages handover of NFC to other technologies.
40  */
41 public class HandoverDataParser {
42     private static final String TAG = "NfcHandover";
43     private static final boolean DBG = false;
44 
45     private static final byte[] TYPE_BT_OOB = "application/vnd.bluetooth.ep.oob"
46             .getBytes(StandardCharsets.US_ASCII);
47     private static final byte[] TYPE_BLE_OOB = "application/vnd.bluetooth.le.oob"
48             .getBytes(StandardCharsets.US_ASCII);
49 
50     private static final byte[] TYPE_NOKIA = "nokia.com:bt".getBytes(StandardCharsets.US_ASCII);
51 
52     private static final byte[] RTD_COLLISION_RESOLUTION = {0x63, 0x72}; // "cr";
53 
54     private static final int CARRIER_POWER_STATE_INACTIVE = 0;
55     private static final int CARRIER_POWER_STATE_ACTIVE = 1;
56     private static final int CARRIER_POWER_STATE_ACTIVATING = 2;
57     private static final int CARRIER_POWER_STATE_UNKNOWN = 3;
58 
59     private static final int BT_HANDOVER_TYPE_MAC = 0x1B;
60     private static final int BT_HANDOVER_TYPE_LE_ROLE = 0x1C;
61     private static final int BT_HANDOVER_TYPE_LONG_LOCAL_NAME = 0x09;
62     private static final int BT_HANDOVER_TYPE_SHORT_LOCAL_NAME = 0x08;
63     private static final int BT_HANDOVER_TYPE_SECURITY_MANAGER_TK = 0x10;
64 
65     public static final int BT_HANDOVER_LE_ROLE_CENTRAL_ONLY = 0x01;
66 
67     public static final int SECURITY_MANAGER_TK_SIZE = 16;
68 
69     private final BluetoothAdapter mBluetoothAdapter;
70 
71     private final Object mLock = new Object();
72     // Variables below synchronized on mLock
73 
74     private String mLocalBluetoothAddress;
75 
76     public static class BluetoothHandoverData {
77         public boolean valid = false;
78         public BluetoothDevice device;
79         public String name;
80         public boolean carrierActivating = false;
81         public int transport = BluetoothDevice.TRANSPORT_AUTO;
82         public OobData oobData;
83     }
84 
85     public static class IncomingHandoverData {
86         public final NdefMessage handoverSelect;
87         public final BluetoothHandoverData handoverData;
88 
IncomingHandoverData(NdefMessage handoverSelect, BluetoothHandoverData handoverData)89         public IncomingHandoverData(NdefMessage handoverSelect,
90                                     BluetoothHandoverData handoverData) {
91             this.handoverSelect = handoverSelect;
92             this.handoverData = handoverData;
93         }
94     }
95 
HandoverDataParser()96     public HandoverDataParser() {
97         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
98     }
99 
createCollisionRecord()100     static NdefRecord createCollisionRecord() {
101         byte[] random = new byte[2];
102         new Random().nextBytes(random);
103         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, RTD_COLLISION_RESOLUTION, null, random);
104     }
105 
createBluetoothAlternateCarrierRecord(boolean activating)106     NdefRecord createBluetoothAlternateCarrierRecord(boolean activating) {
107         byte[] payload = new byte[4];
108         payload[0] = (byte) (activating ? CARRIER_POWER_STATE_ACTIVATING :
109             CARRIER_POWER_STATE_ACTIVE);  // Carrier Power State: Activating or active
110         payload[1] = 1;   // length of carrier data reference
111         payload[2] = 'b'; // carrier data reference: ID for Bluetooth OOB data record
112         payload[3] = 0;  // Auxiliary data reference count
113         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_ALTERNATIVE_CARRIER, null,
114                 payload);
115     }
116 
createBluetoothOobDataRecord()117     NdefRecord createBluetoothOobDataRecord() {
118         byte[] payload = new byte[8];
119         // Note: this field should be little-endian per the BTSSP spec
120         // The Android 4.1 implementation used big-endian order here.
121         // No single Android implementation has ever interpreted this
122         // length field when parsing this record though.
123         payload[0] = (byte) (payload.length & 0xFF);
124         payload[1] = (byte) ((payload.length >> 8) & 0xFF);
125 
126         synchronized (mLock) {
127             if (mLocalBluetoothAddress == null) {
128                 mLocalBluetoothAddress = mBluetoothAdapter.getAddress();
129             }
130 
131             byte[] addressBytes = addressToReverseBytes(mLocalBluetoothAddress);
132             System.arraycopy(addressBytes, 0, payload, 2, 6);
133         }
134 
135         return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, new byte[]{'b'}, payload);
136     }
137 
isHandoverSupported()138     public boolean isHandoverSupported() {
139         return (mBluetoothAdapter != null);
140     }
141 
createHandoverRequestMessage()142     public NdefMessage createHandoverRequestMessage() {
143         if (mBluetoothAdapter == null) {
144             return null;
145         }
146 
147         NdefRecord[] dataRecords = new NdefRecord[] {
148                 createBluetoothOobDataRecord()
149         };
150         return new NdefMessage(
151                 createHandoverRequestRecord(),
152                 dataRecords);
153     }
154 
createBluetoothHandoverSelectMessage(boolean activating)155     NdefMessage createBluetoothHandoverSelectMessage(boolean activating) {
156         return new NdefMessage(createHandoverSelectRecord(
157                 createBluetoothAlternateCarrierRecord(activating)),
158                 createBluetoothOobDataRecord());
159     }
160 
createHandoverSelectRecord(NdefRecord alternateCarrier)161     NdefRecord createHandoverSelectRecord(NdefRecord alternateCarrier) {
162         NdefMessage nestedMessage = new NdefMessage(alternateCarrier);
163         byte[] nestedPayload = nestedMessage.toByteArray();
164 
165         ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
166         payload.put((byte)0x12);  // connection handover v1.2
167         payload.put(nestedPayload);
168 
169         byte[] payloadBytes = new byte[payload.position()];
170         payload.position(0);
171         payload.get(payloadBytes);
172         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null,
173                 payloadBytes);
174     }
175 
createHandoverRequestRecord()176     NdefRecord createHandoverRequestRecord() {
177         NdefRecord[] messages = new NdefRecord[] {
178                 createBluetoothAlternateCarrierRecord(false)
179         };
180 
181         NdefMessage nestedMessage = new NdefMessage(createCollisionRecord(), messages);
182 
183         byte[] nestedPayload = nestedMessage.toByteArray();
184 
185         ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
186         payload.put((byte) 0x12);  // connection handover v1.2
187         payload.put(nestedMessage.toByteArray());
188 
189         byte[] payloadBytes = new byte[payload.position()];
190         payload.position(0);
191         payload.get(payloadBytes);
192         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null,
193                 payloadBytes);
194     }
195 
196     /**
197      * Returns null if message is not a Handover Request,
198      * returns the IncomingHandoverData (Hs + parsed data) if it is.
199      */
getIncomingHandoverData(NdefMessage handoverRequest)200     public IncomingHandoverData getIncomingHandoverData(NdefMessage handoverRequest) {
201         if (handoverRequest == null) return null;
202         if (mBluetoothAdapter == null) return null;
203 
204         if (DBG) Log.d(TAG, "getIncomingHandoverData():" + handoverRequest.toString());
205 
206         NdefRecord handoverRequestRecord = handoverRequest.getRecords()[0];
207         if (handoverRequestRecord.getTnf() != NdefRecord.TNF_WELL_KNOWN) {
208             return null;
209         }
210 
211         if (!Arrays.equals(handoverRequestRecord.getType(), NdefRecord.RTD_HANDOVER_REQUEST)) {
212             return null;
213         }
214 
215         // we have a handover request, look for BT OOB record
216         BluetoothHandoverData bluetoothData = null;
217         for (NdefRecord dataRecord : handoverRequest.getRecords()) {
218             if (dataRecord.getTnf() == NdefRecord.TNF_MIME_MEDIA) {
219                 if (Arrays.equals(dataRecord.getType(), TYPE_BT_OOB)) {
220                     bluetoothData = parseBtOob(ByteBuffer.wrap(dataRecord.getPayload()));
221                 }
222             }
223         }
224 
225         NdefMessage hs = tryBluetoothHandoverRequest(bluetoothData);
226         if (hs != null) {
227             return new IncomingHandoverData(hs, bluetoothData);
228         }
229 
230         return null;
231     }
232 
getOutgoingHandoverData(NdefMessage handoverSelect)233     public BluetoothHandoverData getOutgoingHandoverData(NdefMessage handoverSelect) {
234         return parseBluetooth(handoverSelect);
235     }
236 
tryBluetoothHandoverRequest(BluetoothHandoverData bluetoothData)237     private NdefMessage tryBluetoothHandoverRequest(BluetoothHandoverData bluetoothData) {
238         NdefMessage selectMessage = null;
239         if (bluetoothData != null) {
240             // Note: there could be a race where we conclude
241             // that Bluetooth is already enabled, and shortly
242             // after the user turns it off. That will cause
243             // the transfer to fail, but there's nothing
244             // much we can do about it anyway. It shouldn't
245             // be common for the user to be changing BT settings
246             // while waiting to receive a picture.
247             boolean bluetoothActivating = !mBluetoothAdapter.isEnabled();
248 
249             // return BT OOB record so they can perform handover
250             selectMessage = (createBluetoothHandoverSelectMessage(bluetoothActivating));
251             if (DBG) Log.d(TAG, "Waiting for incoming transfer, [" +
252                     bluetoothData.device.getAddress() + "]->[" + mLocalBluetoothAddress + "]");
253         }
254 
255         return selectMessage;
256     }
257 
258 
259 
isCarrierActivating(NdefRecord handoverRec, byte[] carrierId)260     boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) {
261         byte[] payload = handoverRec.getPayload();
262         if (payload == null || payload.length <= 1) return false;
263         // Skip version
264         byte[] payloadNdef = new byte[payload.length - 1];
265         System.arraycopy(payload, 1, payloadNdef, 0, payload.length - 1);
266         NdefMessage msg;
267         try {
268             msg = new NdefMessage(payloadNdef);
269         } catch (FormatException e) {
270             return false;
271         }
272 
273         for (NdefRecord alt : msg.getRecords()) {
274             byte[] acPayload = alt.getPayload();
275             if (acPayload != null) {
276                 ByteBuffer buf = ByteBuffer.wrap(acPayload);
277                 int cps = buf.get() & 0x03; // Carrier Power State is in lower 2 bits
278                 int carrierRefLength = buf.get() & 0xFF;
279                 if (carrierRefLength != carrierId.length) return false;
280 
281                 byte[] carrierRefId = new byte[carrierRefLength];
282                 buf.get(carrierRefId);
283                 if (Arrays.equals(carrierRefId, carrierId)) {
284                     // Found match, returning whether power state is activating
285                     return (cps == CARRIER_POWER_STATE_ACTIVATING);
286                 }
287             }
288         }
289 
290         return true;
291     }
292 
parseBluetoothHandoverSelect(NdefMessage m)293     BluetoothHandoverData parseBluetoothHandoverSelect(NdefMessage m) {
294         // TODO we could parse this a lot more strictly; right now
295         // we just search for a BT OOB record, and try to cross-reference
296         // the carrier state inside the 'hs' payload.
297         for (NdefRecord oob : m.getRecords()) {
298             if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
299                     Arrays.equals(oob.getType(), TYPE_BT_OOB)) {
300                 BluetoothHandoverData data = parseBtOob(ByteBuffer.wrap(oob.getPayload()));
301                 if (data != null && isCarrierActivating(m.getRecords()[0], oob.getId())) {
302                     data.carrierActivating = true;
303                 }
304                 return data;
305             }
306 
307             if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
308                     Arrays.equals(oob.getType(), TYPE_BLE_OOB)) {
309                 return parseBleOob(ByteBuffer.wrap(oob.getPayload()));
310             }
311         }
312 
313         return null;
314     }
315 
parseBluetooth(NdefMessage m)316     public BluetoothHandoverData parseBluetooth(NdefMessage m) {
317         NdefRecord r = m.getRecords()[0];
318         short tnf = r.getTnf();
319         byte[] type = r.getType();
320 
321         // Check for BT OOB record
322         if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BT_OOB)) {
323             return parseBtOob(ByteBuffer.wrap(r.getPayload()));
324         }
325 
326         // Check for BLE OOB record
327         if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BLE_OOB)) {
328             return parseBleOob(ByteBuffer.wrap(r.getPayload()));
329         }
330 
331         // Check for Handover Select, followed by a BT OOB record
332         if (tnf == NdefRecord.TNF_WELL_KNOWN &&
333                 Arrays.equals(type, NdefRecord.RTD_HANDOVER_SELECT)) {
334             return parseBluetoothHandoverSelect(m);
335         }
336 
337         // Check for Nokia BT record, found on some Nokia BH-505 Headsets
338         if (tnf == NdefRecord.TNF_EXTERNAL_TYPE && Arrays.equals(type, TYPE_NOKIA)) {
339             return parseNokia(ByteBuffer.wrap(r.getPayload()));
340         }
341 
342         return null;
343     }
344 
parseNokia(ByteBuffer payload)345     BluetoothHandoverData parseNokia(ByteBuffer payload) {
346         BluetoothHandoverData result = new BluetoothHandoverData();
347         result.valid = false;
348 
349         try {
350             payload.position(1);
351             byte[] address = new byte[6];
352             payload.get(address);
353             result.device = mBluetoothAdapter.getRemoteDevice(address);
354             result.valid = true;
355             payload.position(14);
356             int nameLength = payload.get();
357             byte[] nameBytes = new byte[nameLength];
358             payload.get(nameBytes);
359             result.name = new String(nameBytes, StandardCharsets.UTF_8);
360         } catch (IllegalArgumentException e) {
361             Log.i(TAG, "nokia: invalid BT address");
362         } catch (BufferUnderflowException e) {
363             Log.i(TAG, "nokia: payload shorter than expected");
364         }
365         if (result.valid && result.name == null) result.name = "";
366         return result;
367     }
368 
parseBtOob(ByteBuffer payload)369     BluetoothHandoverData parseBtOob(ByteBuffer payload) {
370         BluetoothHandoverData result = new BluetoothHandoverData();
371         result.valid = false;
372 
373         try {
374             payload.position(2); // length
375             byte[] address = parseMacFromBluetoothRecord(payload);
376             result.device = mBluetoothAdapter.getRemoteDevice(address);
377             result.valid = true;
378 
379             while (payload.remaining() > 0) {
380                 byte[] nameBytes;
381                 int len = payload.get();
382                 int type = payload.get();
383                 switch (type) {
384                     case BT_HANDOVER_TYPE_SHORT_LOCAL_NAME:
385                         nameBytes = new byte[len - 1];
386                         payload.get(nameBytes);
387                         result.name = new String(nameBytes, StandardCharsets.UTF_8);
388                         break;
389                     case BT_HANDOVER_TYPE_LONG_LOCAL_NAME:
390                         if (result.name != null) break;  // prefer short name
391                         nameBytes = new byte[len - 1];
392                         payload.get(nameBytes);
393                         result.name = new String(nameBytes, StandardCharsets.UTF_8);
394                         break;
395                     default:
396                         payload.position(payload.position() + len - 1);
397                         break;
398                 }
399             }
400         } catch (IllegalArgumentException e) {
401             Log.i(TAG, "BT OOB: invalid BT address");
402         } catch (BufferUnderflowException e) {
403             Log.i(TAG, "BT OOB: payload shorter than expected");
404         }
405         if (result.valid && result.name == null) result.name = "";
406         return result;
407     }
408 
parseBleOob(ByteBuffer payload)409     BluetoothHandoverData parseBleOob(ByteBuffer payload) {
410         BluetoothHandoverData result = new BluetoothHandoverData();
411         result.valid = false;
412         result.transport = BluetoothDevice.TRANSPORT_LE;
413 
414         try {
415 
416             while (payload.remaining() > 0) {
417                 int len = payload.get();
418                 int type = payload.get();
419                 switch (type) {
420                     case BT_HANDOVER_TYPE_MAC: // mac address
421                         byte[] address = parseMacFromBluetoothRecord(payload);
422                         payload.position(payload.position() + 1); // advance over random byte
423                         result.device = mBluetoothAdapter.getRemoteDevice(address);
424                         result.valid = true;
425                         break;
426                     case BT_HANDOVER_TYPE_LE_ROLE:
427                         byte role = payload.get();
428                         if (role == BT_HANDOVER_LE_ROLE_CENTRAL_ONLY) {
429                             // only central role supported, can't pair
430                             result.valid = false;
431                             return result;
432                         }
433                         break;
434                     case BT_HANDOVER_TYPE_LONG_LOCAL_NAME:
435                         byte[] nameBytes = new byte[len - 1];
436                         payload.get(nameBytes);
437                         result.name = new String(nameBytes, StandardCharsets.UTF_8);
438                         break;
439                     case BT_HANDOVER_TYPE_SECURITY_MANAGER_TK:
440                         if (len-1 != SECURITY_MANAGER_TK_SIZE) {
441                             Log.i(TAG, "BT OOB: invalid size of SM TK, should be " +
442                                   SECURITY_MANAGER_TK_SIZE + " bytes.");
443                             break;
444                         }
445 
446                         byte[] reversedTK = new byte[len - 1];
447                         payload.get(reversedTK);
448 
449                         byte[] securityManagerTK = new byte[len - 1];
450 
451                         //TK in AD is in reverse order
452                         for (int i = 0; i < reversedTK.length; i++) {
453                             securityManagerTK[i] = reversedTK[securityManagerTK.length - 1 - i];
454                         }
455 
456                         result.oobData = new OobData();
457                         result.oobData.setSecurityManagerTk(securityManagerTK);
458                         break;
459                     default:
460                         payload.position(payload.position() + len - 1);
461                         break;
462                 }
463             }
464         } catch (IllegalArgumentException e) {
465             Log.i(TAG, "BT OOB: invalid BT address");
466         } catch (BufferUnderflowException e) {
467             Log.i(TAG, "BT OOB: payload shorter than expected");
468         }
469         if (result.valid && result.name == null) result.name = "";
470         return result;
471     }
472 
parseMacFromBluetoothRecord(ByteBuffer payload)473     private byte[] parseMacFromBluetoothRecord(ByteBuffer payload) {
474         byte[] address = new byte[6];
475         payload.get(address);
476         // ByteBuffer.order(LITTLE_ENDIAN) doesn't work for
477         // ByteBuffer.get(byte[]), so manually swap order
478         for (int i = 0; i < 3; i++) {
479             byte temp = address[i];
480             address[i] = address[5 - i];
481             address[5 - i] = temp;
482         }
483         return address;
484     }
485 
addressToReverseBytes(String address)486     static byte[] addressToReverseBytes(String address) {
487         String[] split = address.split(":");
488         byte[] result = new byte[split.length];
489 
490         for (int i = 0; i < split.length; i++) {
491             // need to parse as int because parseByte() expects a signed byte
492             result[split.length - 1 - i] = (byte)Integer.parseInt(split[i], 16);
493         }
494 
495         return result;
496     }
497 }
498 
499