Home | History | Annotate | Download | only in handover
      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 
     89         public IncomingHandoverData(NdefMessage handoverSelect,
     90                                     BluetoothHandoverData handoverData) {
     91             this.handoverSelect = handoverSelect;
     92             this.handoverData = handoverData;
     93         }
     94     }
     95 
     96     public HandoverDataParser() {
     97         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
     98     }
     99 
    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 
    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 
    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 
    138     public boolean isHandoverSupported() {
    139         return (mBluetoothAdapter != null);
    140     }
    141 
    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 
    155     NdefMessage createBluetoothHandoverSelectMessage(boolean activating) {
    156         return new NdefMessage(createHandoverSelectRecord(
    157                 createBluetoothAlternateCarrierRecord(activating)),
    158                 createBluetoothOobDataRecord());
    159     }
    160 
    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 
    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      */
    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 
    233     public BluetoothHandoverData getOutgoingHandoverData(NdefMessage handoverSelect) {
    234         return parseBluetooth(handoverSelect);
    235     }
    236 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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