• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     private static final int BT_HANDOVER_TYPE_APPEARANCE = 0x19;
65     private static final int BT_HANDOVER_TYPE_LE_SC_CONFIRMATION = 0x22;
66     private static final int BT_HANDOVER_TYPE_LE_SC_RANDOM = 0x23;
67 
68     public static final int BT_HANDOVER_LE_ROLE_CENTRAL_ONLY = 0x01;
69 
70     public static final int SECURITY_MANAGER_TK_SIZE = 16;
71     public static final int SECURITY_MANAGER_LE_SC_C_SIZE = 16;
72     public static final int SECURITY_MANAGER_LE_SC_R_SIZE = 16;
73 
74     private final BluetoothAdapter mBluetoothAdapter;
75 
76     private final Object mLock = new Object();
77     // Variables below synchronized on mLock
78 
79     private String mLocalBluetoothAddress;
80 
81     public static class BluetoothHandoverData {
82         public boolean valid = false;
83         public BluetoothDevice device;
84         public String name;
85         public boolean carrierActivating = false;
86         public int transport = BluetoothDevice.TRANSPORT_AUTO;
87         public OobData oobData;
88     }
89 
90     public static class IncomingHandoverData {
91         public final NdefMessage handoverSelect;
92         public final BluetoothHandoverData handoverData;
93 
IncomingHandoverData(NdefMessage handoverSelect, BluetoothHandoverData handoverData)94         public IncomingHandoverData(NdefMessage handoverSelect,
95                                     BluetoothHandoverData handoverData) {
96             this.handoverSelect = handoverSelect;
97             this.handoverData = handoverData;
98         }
99     }
100 
HandoverDataParser()101     public HandoverDataParser() {
102         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
103     }
104 
createCollisionRecord()105     static NdefRecord createCollisionRecord() {
106         byte[] random = new byte[2];
107         new Random().nextBytes(random);
108         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, RTD_COLLISION_RESOLUTION, null, random);
109     }
110 
createBluetoothAlternateCarrierRecord(boolean activating)111     NdefRecord createBluetoothAlternateCarrierRecord(boolean activating) {
112         byte[] payload = new byte[4];
113         payload[0] = (byte) (activating ? CARRIER_POWER_STATE_ACTIVATING :
114             CARRIER_POWER_STATE_ACTIVE);  // Carrier Power State: Activating or active
115         payload[1] = 1;   // length of carrier data reference
116         payload[2] = 'b'; // carrier data reference: ID for Bluetooth OOB data record
117         payload[3] = 0;  // Auxiliary data reference count
118         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_ALTERNATIVE_CARRIER, null,
119                 payload);
120     }
121 
createBluetoothOobDataRecord()122     NdefRecord createBluetoothOobDataRecord() {
123         byte[] payload = new byte[8];
124         // Note: this field should be little-endian per the BTSSP spec
125         // The Android 4.1 implementation used big-endian order here.
126         // No single Android implementation has ever interpreted this
127         // length field when parsing this record though.
128         payload[0] = (byte) (payload.length & 0xFF);
129         payload[1] = (byte) ((payload.length >> 8) & 0xFF);
130 
131         synchronized (mLock) {
132             if (mLocalBluetoothAddress == null) {
133                 mLocalBluetoothAddress = mBluetoothAdapter.getAddress();
134             }
135 
136             byte[] addressBytes = addressToReverseBytes(mLocalBluetoothAddress);
137             System.arraycopy(addressBytes, 0, payload, 2, 6);
138         }
139 
140         return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, new byte[]{'b'}, payload);
141     }
142 
isHandoverSupported()143     public boolean isHandoverSupported() {
144         return (mBluetoothAdapter != null);
145     }
146 
createHandoverRequestMessage()147     public NdefMessage createHandoverRequestMessage() {
148         if (mBluetoothAdapter == null) {
149             return null;
150         }
151 
152         NdefRecord[] dataRecords = new NdefRecord[] {
153                 createBluetoothOobDataRecord()
154         };
155         return new NdefMessage(
156                 createHandoverRequestRecord(),
157                 dataRecords);
158     }
159 
createBluetoothHandoverSelectMessage(boolean activating)160     NdefMessage createBluetoothHandoverSelectMessage(boolean activating) {
161         return new NdefMessage(createHandoverSelectRecord(
162                 createBluetoothAlternateCarrierRecord(activating)),
163                 createBluetoothOobDataRecord());
164     }
165 
createHandoverSelectRecord(NdefRecord alternateCarrier)166     NdefRecord createHandoverSelectRecord(NdefRecord alternateCarrier) {
167         NdefMessage nestedMessage = new NdefMessage(alternateCarrier);
168         byte[] nestedPayload = nestedMessage.toByteArray();
169 
170         ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
171         payload.put((byte)0x12);  // connection handover v1.2
172         payload.put(nestedPayload);
173 
174         byte[] payloadBytes = new byte[payload.position()];
175         payload.position(0);
176         payload.get(payloadBytes);
177         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null,
178                 payloadBytes);
179     }
180 
createHandoverRequestRecord()181     NdefRecord createHandoverRequestRecord() {
182         NdefRecord[] messages = new NdefRecord[] {
183                 createBluetoothAlternateCarrierRecord(false)
184         };
185 
186         NdefMessage nestedMessage = new NdefMessage(createCollisionRecord(), messages);
187 
188         byte[] nestedPayload = nestedMessage.toByteArray();
189 
190         ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
191         payload.put((byte) 0x12);  // connection handover v1.2
192         payload.put(nestedMessage.toByteArray());
193 
194         byte[] payloadBytes = new byte[payload.position()];
195         payload.position(0);
196         payload.get(payloadBytes);
197         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null,
198                 payloadBytes);
199     }
200 
201     /**
202      * Returns null if message is not a Handover Request,
203      * returns the IncomingHandoverData (Hs + parsed data) if it is.
204      */
getIncomingHandoverData(NdefMessage handoverRequest)205     public IncomingHandoverData getIncomingHandoverData(NdefMessage handoverRequest) {
206         if (handoverRequest == null) return null;
207         if (mBluetoothAdapter == null) return null;
208 
209         if (DBG) Log.d(TAG, "getIncomingHandoverData():" + handoverRequest.toString());
210 
211         NdefRecord handoverRequestRecord = handoverRequest.getRecords()[0];
212         if (handoverRequestRecord.getTnf() != NdefRecord.TNF_WELL_KNOWN) {
213             return null;
214         }
215 
216         if (!Arrays.equals(handoverRequestRecord.getType(), NdefRecord.RTD_HANDOVER_REQUEST)) {
217             return null;
218         }
219 
220         // we have a handover request, look for BT OOB record
221         BluetoothHandoverData bluetoothData = null;
222         for (NdefRecord dataRecord : handoverRequest.getRecords()) {
223             if (dataRecord.getTnf() == NdefRecord.TNF_MIME_MEDIA) {
224                 if (Arrays.equals(dataRecord.getType(), TYPE_BT_OOB)) {
225                     bluetoothData = parseBtOob(ByteBuffer.wrap(dataRecord.getPayload()));
226                 }
227             }
228         }
229 
230         NdefMessage hs = tryBluetoothHandoverRequest(bluetoothData);
231         if (hs != null) {
232             return new IncomingHandoverData(hs, bluetoothData);
233         }
234 
235         return null;
236     }
237 
getOutgoingHandoverData(NdefMessage handoverSelect)238     public BluetoothHandoverData getOutgoingHandoverData(NdefMessage handoverSelect) {
239         return parseBluetooth(handoverSelect);
240     }
241 
tryBluetoothHandoverRequest(BluetoothHandoverData bluetoothData)242     private NdefMessage tryBluetoothHandoverRequest(BluetoothHandoverData bluetoothData) {
243         NdefMessage selectMessage = null;
244         if (bluetoothData != null) {
245             // Note: there could be a race where we conclude
246             // that Bluetooth is already enabled, and shortly
247             // after the user turns it off. That will cause
248             // the transfer to fail, but there's nothing
249             // much we can do about it anyway. It shouldn't
250             // be common for the user to be changing BT settings
251             // while waiting to receive a picture.
252             boolean bluetoothActivating = !mBluetoothAdapter.isEnabled();
253 
254             // return BT OOB record so they can perform handover
255             selectMessage = (createBluetoothHandoverSelectMessage(bluetoothActivating));
256             if (DBG) Log.d(TAG, "Waiting for incoming transfer, [" +
257                     bluetoothData.device.getAddress() + "]->[" + mLocalBluetoothAddress + "]");
258         }
259 
260         return selectMessage;
261     }
262 
263 
264 
isCarrierActivating(NdefRecord handoverRec, byte[] carrierId)265     boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) {
266         byte[] payload = handoverRec.getPayload();
267         if (payload == null || payload.length <= 1) return false;
268         // Skip version
269         byte[] payloadNdef = new byte[payload.length - 1];
270         System.arraycopy(payload, 1, payloadNdef, 0, payload.length - 1);
271         NdefMessage msg;
272         try {
273             msg = new NdefMessage(payloadNdef);
274         } catch (FormatException e) {
275             return false;
276         }
277 
278         for (NdefRecord alt : msg.getRecords()) {
279             byte[] acPayload = alt.getPayload();
280             if (acPayload != null) {
281                 ByteBuffer buf = ByteBuffer.wrap(acPayload);
282                 int cps = buf.get() & 0x03; // Carrier Power State is in lower 2 bits
283                 int carrierRefLength = buf.get() & 0xFF;
284                 if (carrierRefLength != carrierId.length) return false;
285 
286                 byte[] carrierRefId = new byte[carrierRefLength];
287                 buf.get(carrierRefId);
288                 if (Arrays.equals(carrierRefId, carrierId)) {
289                     // Found match, returning whether power state is activating
290                     return (cps == CARRIER_POWER_STATE_ACTIVATING);
291                 }
292             }
293         }
294 
295         return true;
296     }
297 
parseBluetoothHandoverSelect(NdefMessage m)298     BluetoothHandoverData parseBluetoothHandoverSelect(NdefMessage m) {
299         // TODO we could parse this a lot more strictly; right now
300         // we just search for a BT OOB record, and try to cross-reference
301         // the carrier state inside the 'hs' payload.
302         for (NdefRecord oob : m.getRecords()) {
303             if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
304                     Arrays.equals(oob.getType(), TYPE_BT_OOB)) {
305                 BluetoothHandoverData data = parseBtOob(ByteBuffer.wrap(oob.getPayload()));
306                 if (data != null && isCarrierActivating(m.getRecords()[0], oob.getId())) {
307                     data.carrierActivating = true;
308                 }
309                 return data;
310             }
311 
312             if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
313                     Arrays.equals(oob.getType(), TYPE_BLE_OOB)) {
314                 return parseBleOob(ByteBuffer.wrap(oob.getPayload()));
315             }
316         }
317 
318         return null;
319     }
320 
parseBluetooth(NdefMessage m)321     public BluetoothHandoverData parseBluetooth(NdefMessage m) {
322         NdefRecord r = m.getRecords()[0];
323         short tnf = r.getTnf();
324         byte[] type = r.getType();
325 
326         // Check for BT OOB record
327         if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BT_OOB)) {
328             return parseBtOob(ByteBuffer.wrap(r.getPayload()));
329         }
330 
331         // Check for BLE OOB record
332         if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BLE_OOB)) {
333             return parseBleOob(ByteBuffer.wrap(r.getPayload()));
334         }
335 
336         // Check for Handover Select, followed by a BT OOB record
337         if (tnf == NdefRecord.TNF_WELL_KNOWN &&
338                 Arrays.equals(type, NdefRecord.RTD_HANDOVER_SELECT)) {
339             return parseBluetoothHandoverSelect(m);
340         }
341 
342         // Check for Nokia BT record, found on some Nokia BH-505 Headsets
343         if (tnf == NdefRecord.TNF_EXTERNAL_TYPE && Arrays.equals(type, TYPE_NOKIA)) {
344             return parseNokia(ByteBuffer.wrap(r.getPayload()));
345         }
346 
347         return null;
348     }
349 
parseNokia(ByteBuffer payload)350     BluetoothHandoverData parseNokia(ByteBuffer payload) {
351         BluetoothHandoverData result = new BluetoothHandoverData();
352         result.valid = false;
353 
354         try {
355             payload.position(1);
356             byte[] address = new byte[6];
357             payload.get(address);
358             result.device = mBluetoothAdapter.getRemoteDevice(address);
359             result.valid = true;
360             payload.position(14);
361             int nameLength = payload.get();
362             byte[] nameBytes = new byte[nameLength];
363             payload.get(nameBytes);
364             result.name = new String(nameBytes, StandardCharsets.UTF_8);
365         } catch (IllegalArgumentException e) {
366             Log.i(TAG, "nokia: invalid BT address");
367         } catch (BufferUnderflowException e) {
368             Log.i(TAG, "nokia: payload shorter than expected");
369         }
370         if (result.valid && result.name == null) result.name = "";
371         return result;
372     }
373 
parseBtOob(ByteBuffer payload)374     BluetoothHandoverData parseBtOob(ByteBuffer payload) {
375         BluetoothHandoverData result = new BluetoothHandoverData();
376         result.valid = false;
377 
378         try {
379             payload.position(2); // length
380             byte[] address = parseMacFromBluetoothRecord(payload);
381             result.device = mBluetoothAdapter.getRemoteDevice(address);
382             result.valid = true;
383 
384             while (payload.remaining() > 0) {
385                 byte[] nameBytes;
386                 int len = payload.get();
387                 int type = payload.get();
388                 switch (type) {
389                     case BT_HANDOVER_TYPE_SHORT_LOCAL_NAME:
390                         nameBytes = new byte[len - 1];
391                         payload.get(nameBytes);
392                         result.name = new String(nameBytes, StandardCharsets.UTF_8);
393                         break;
394                     case BT_HANDOVER_TYPE_LONG_LOCAL_NAME:
395                         if (result.name != null) break;  // prefer short name
396                         nameBytes = new byte[len - 1];
397                         payload.get(nameBytes);
398                         result.name = new String(nameBytes, StandardCharsets.UTF_8);
399                         break;
400                     default:
401                         payload.position(payload.position() + len - 1);
402                         break;
403                 }
404             }
405         } catch (IllegalArgumentException e) {
406             Log.i(TAG, "BT OOB: invalid BT address");
407         } catch (BufferUnderflowException e) {
408             Log.i(TAG, "BT OOB: payload shorter than expected");
409         }
410         if (result.valid && result.name == null) result.name = "";
411         return result;
412     }
413 
parseBleOob(ByteBuffer payload)414     BluetoothHandoverData parseBleOob(ByteBuffer payload) {
415         BluetoothHandoverData result = new BluetoothHandoverData();
416         result.valid = false;
417         result.transport = BluetoothDevice.TRANSPORT_LE;
418 
419         try {
420 
421             while (payload.remaining() > 0) {
422                 int len = payload.get();
423                 int type = payload.get();
424                 switch (type) {
425                     case BT_HANDOVER_TYPE_MAC: // mac address
426 
427                         int startpos = payload.position();
428                         byte[] bdaddr = new byte[7]; // 6 bytes for mac, 1 for addres type
429                         payload.get(bdaddr);
430                         if (result.oobData == null)
431                             result.oobData = new OobData();
432                         result.oobData.setLeBluetoothDeviceAddress(bdaddr);
433                         payload.position(startpos);
434 
435                         byte[] address = parseMacFromBluetoothRecord(payload);
436                         payload.position(payload.position() + 1); // advance over random byte
437                         result.device = mBluetoothAdapter.getRemoteDevice(address);
438                         result.valid = true;
439                         break;
440                     case BT_HANDOVER_TYPE_LE_ROLE:
441                         byte role = payload.get();
442                         if (role == BT_HANDOVER_LE_ROLE_CENTRAL_ONLY) {
443                             // only central role supported, can't pair
444                             result.valid = false;
445                             return result;
446                         }
447                         break;
448                     case BT_HANDOVER_TYPE_LONG_LOCAL_NAME:
449                         byte[] nameBytes = new byte[len - 1];
450                         payload.get(nameBytes);
451                         result.name = new String(nameBytes, StandardCharsets.UTF_8);
452                         break;
453                     case BT_HANDOVER_TYPE_SECURITY_MANAGER_TK:
454                         if (len-1 != SECURITY_MANAGER_TK_SIZE) {
455                             Log.i(TAG, "BT OOB: invalid size of SM TK, should be " +
456                                   SECURITY_MANAGER_TK_SIZE + " bytes.");
457                             break;
458                         }
459 
460                         byte[] securityManagerTK = new byte[len - 1];
461                         payload.get(securityManagerTK);
462 
463                         if (result.oobData == null)
464                             result.oobData = new OobData();
465                         result.oobData.setSecurityManagerTk(securityManagerTK);
466                         break;
467 
468                     case BT_HANDOVER_TYPE_LE_SC_CONFIRMATION:
469                         if (len - 1 != SECURITY_MANAGER_LE_SC_C_SIZE) {
470                             Log.i(TAG, "BT OOB: invalid size of LE SC Confirmation, should be " +
471                                   SECURITY_MANAGER_LE_SC_C_SIZE + " bytes.");
472                             break;
473                         }
474 
475                         byte[] leScC = new byte[len - 1];
476                         payload.get(leScC);
477 
478                         if (result.oobData == null)
479                             result.oobData = new OobData();
480                         result.oobData.setLeSecureConnectionsConfirmation(leScC);
481                         break;
482 
483                     case BT_HANDOVER_TYPE_LE_SC_RANDOM:
484                         if (len-1 != SECURITY_MANAGER_LE_SC_R_SIZE) {
485                             Log.i(TAG, "BT OOB: invalid size of LE SC Random, should be " +
486                                   SECURITY_MANAGER_LE_SC_R_SIZE + " bytes.");
487                             break;
488                         }
489 
490                         byte[] leScR = new byte[len - 1];
491                         payload.get(leScR);
492 
493                         if (result.oobData == null)
494                             result.oobData = new OobData();
495                         result.oobData.setLeSecureConnectionsRandom(leScR);
496                         break;
497 
498                     default:
499                         payload.position(payload.position() + len - 1);
500                         break;
501                 }
502             }
503         } catch (IllegalArgumentException e) {
504             Log.i(TAG, "BLE OOB: error parsing OOB data", e);
505         } catch (BufferUnderflowException e) {
506             Log.i(TAG, "BT OOB: payload shorter than expected");
507         }
508         if (result.valid && result.name == null) result.name = "";
509         return result;
510     }
511 
parseMacFromBluetoothRecord(ByteBuffer payload)512     private byte[] parseMacFromBluetoothRecord(ByteBuffer payload) {
513         byte[] address = new byte[6];
514         payload.get(address);
515         // ByteBuffer.order(LITTLE_ENDIAN) doesn't work for
516         // ByteBuffer.get(byte[]), so manually swap order
517         for (int i = 0; i < 3; i++) {
518             byte temp = address[i];
519             address[i] = address[5 - i];
520             address[5 - i] = temp;
521         }
522         return address;
523     }
524 
addressToReverseBytes(String address)525     static byte[] addressToReverseBytes(String address) {
526         String[] split = address.split(":");
527         byte[] result = new byte[split.length];
528 
529         for (int i = 0; i < split.length; i++) {
530             // need to parse as int because parseByte() expects a signed byte
531             result[split.length - 1 - i] = (byte)Integer.parseInt(split[i], 16);
532         }
533 
534         return result;
535     }
536 }
537 
538