/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.bluetooth; import static com.android.car.bluetooth.FastPairUtils.AccountKey; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattServer; import android.bluetooth.BluetoothGattServerCallback; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; import android.os.ParcelUuid; import android.util.Base64; import android.util.IndentingPrintWriter; import android.util.Log; import com.android.car.CarServiceUtils; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.KeyFactory; import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.ECPublicKey; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; import java.security.spec.ECPrivateKeySpec; import java.security.spec.ECPublicKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import javax.crypto.Cipher; import javax.crypto.KeyAgreement; import javax.crypto.spec.SecretKeySpec; /** * The FastPairGattServer is responsible for all 2 way communications with the Fast Pair Seeker. * It is running in the background over BLE whenever the Fast Pair Service is running, waiting for a * Seeker to connect, after which time it manages the authentication an performs the steps as * required by the Fast Pair Specification. */ class FastPairGattServer { // Service ID assigned for FastPair. public static final ParcelUuid FAST_PAIR_SERVICE_UUID = ParcelUuid .fromString("0000FE2C-0000-1000-8000-00805f9b34fb"); public static final ParcelUuid FAST_PAIR_MODEL_ID_UUID = ParcelUuid .fromString("FE2C1233-8366-4814-8EB0-01DE32100BEA"); public static final ParcelUuid KEY_BASED_PAIRING_UUID = ParcelUuid .fromString("FE2C1234-8366-4814-8EB0-01DE32100BEA"); public static final ParcelUuid PASSKEY_UUID = ParcelUuid .fromString("FE2C1235-8366-4814-8EB0-01DE32100BEA"); public static final ParcelUuid ACCOUNT_KEY_UUID = ParcelUuid .fromString("FE2C1236-8366-4814-8EB0-01DE32100BEA"); public static final ParcelUuid CLIENT_CHARACTERISTIC_CONFIG = ParcelUuid .fromString("00002902-0000-1000-8000-00805f9b34fb"); public static final ParcelUuid DEVICE_NAME_CHARACTERISTIC_CONFIG = ParcelUuid .fromString("00002A00-0000-1000-8000-00805f9b34fb"); private static final String TAG = "FastPairGattServer"; private static final boolean DBG = FastPairUtils.DBG; private static final int MAX_KEY_COUNT = 10; private static final int KEY_LIFESPAN = 10_000; private static final int INVALID = -1; private final boolean mAutomaticPasskeyConfirmation; private final byte[] mModelId; private final String mPrivateAntiSpoof; private final Context mContext; private ArrayList mKeys = new ArrayList<>(); private BluetoothGattServer mBluetoothGattServer; private BluetoothManager mBluetoothManager; private int mPairingPasskey = INVALID; private int mFailureCount = 0; private int mSuccessCount = 0; private BluetoothGattService mFastPairService = new BluetoothGattService( FAST_PAIR_SERVICE_UUID.getUuid(), BluetoothGattService.SERVICE_TYPE_PRIMARY); private Callbacks mCallbacks; private SecretKeySpec mSharedSecretKey; private byte[] mEncryptedResponse; private BluetoothDevice mLocalRpaDevice; private BluetoothDevice mRemotePairingDevice; private BluetoothDevice mRemoteGattDevice; interface Callbacks { /** * Notify the Provider of completion to a GATT session * @param successful */ void onPairingCompleted(boolean successful); } /** * Check if a client is connected to this GATT server * @return true if connected; */ public boolean isConnected() { return (mRemoteGattDevice != null); } public void updateLocalRpa(BluetoothDevice device) { mLocalRpaDevice = device; } private Runnable mClearSharedSecretKey = new Runnable() { @Override public void run() { mSharedSecretKey = null; } }; private final Handler mHandler = new Handler( CarServiceUtils.getHandlerThread(FastPairUtils.THREAD_NAME).getLooper()); private BluetoothGattCharacteristic mModelIdCharacteristic; private BluetoothGattCharacteristic mKeyBasedPairingCharacteristic; private BluetoothGattCharacteristic mPasskeyCharacteristic; private BluetoothGattCharacteristic mAccountKeyCharacteristic; private BluetoothGattCharacteristic mDeviceNameCharacteristic; /** * GATT server callbacks responsible for servicing read and write calls from the remote device */ private BluetoothGattServerCallback mBluetoothGattServerCallback = new BluetoothGattServerCallback() { @Override public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { super.onConnectionStateChange(device, status, newState); if (DBG) { Log.d(TAG, "onConnectionStateChange " + newState + "Device: " + device.toString()); } if (newState == 0) { mPairingPasskey = -1; mSharedSecretKey = null; mRemoteGattDevice = null; mCallbacks.onPairingCompleted(false); } else if (newState > 0) { mRemoteGattDevice = device; } } @Override public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { super.onCharacteristicReadRequest(device, requestId, offset, characteristic); if (DBG) { Log.d(TAG, "onCharacteristicReadRequest"); } if (characteristic == mModelIdCharacteristic) { if (DBG) { Log.d(TAG, "reading model ID"); } } mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue()); } @Override public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value); if (DBG) { Log.d(TAG, "onWrite uuid()" + characteristic.getUuid() + "Length" + value.length); } if (characteristic == mAccountKeyCharacteristic) { if (DBG) { Log.d(TAG, "onWriteAccountKeyCharacteristic"); } processAccountKey(value); mBluetoothGattServer .sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue()); } else if (characteristic == mKeyBasedPairingCharacteristic) { if (DBG) { Log.d(TAG, "KeyBasedPairingCharacteristic"); } processKeyBasedPairing(value); mKeyBasedPairingCharacteristic.setValue(mEncryptedResponse); mBluetoothGattServer .sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, mEncryptedResponse); mBluetoothGattServer .notifyCharacteristicChanged(device, mDeviceNameCharacteristic, false); mBluetoothGattServer .notifyCharacteristicChanged(device, mKeyBasedPairingCharacteristic, false); } else if (characteristic == mPasskeyCharacteristic) { if (DBG) { Log.d(TAG, "onWritePasskey" + characteristic.getUuid()); } mBluetoothGattServer .sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, mEncryptedResponse); processPairingKey(value); } else { Log.w(TAG, "onWriteOther" + characteristic.getUuid()); } } @Override public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { if (DBG) { Log.d(TAG, "onDescriptorWriteRequest"); } mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, descriptor.getValue()); } }; /** * Receive incoming pairing requests such that we can confirm Keys match. */ BroadcastReceiver mPairingAttemptsReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DBG) { Log.d(TAG, intent.getAction()); } if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) { mRemotePairingDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); mPairingPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, INVALID); if (DBG) { Log.d(TAG, "DeviceAddress: " + mRemotePairingDevice + " PairingCode: " + mPairingPasskey); } sendPairingResponse(mPairingPasskey); } } }; /** * FastPairGattServer * @param context user specific context on which to make callse * @param modelId assigned Fast Pair Model ID * @param antiSpoof assigned Fast Pair private Anti Spoof key * @param callbacks callbacks used to report back current pairing status * @param automaticAcceptance automatically accept an incoming pairing request that has been * authenticated through the Fast Pair protocol without further user interaction. */ FastPairGattServer(Context context, int modelId, String antiSpoof, Callbacks callbacks, boolean automaticAcceptance) { mContext = context; mCallbacks = callbacks; mPrivateAntiSpoof = antiSpoof; mAutomaticPasskeyConfirmation = automaticAcceptance; mBluetoothManager = context.getSystemService(BluetoothManager.class); mBluetoothGattServer = mBluetoothManager .openGattServer(context, mBluetoothGattServerCallback); if (DBG) { Log.d(TAG, "mBTManager: " + mBluetoothManager.toString() + " GATT: " + mBluetoothGattServer); } ByteBuffer modelIdBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt( modelId); mModelId = Arrays.copyOfRange(modelIdBytes.array(), 0, 3); setup(); } void setSharedSecretKey(byte[] key) { mSharedSecretKey = new SecretKeySpec(key, "AES"); mHandler.postDelayed(mClearSharedSecretKey, KEY_LIFESPAN); } /** * Utilize the key set via setSharedSecretKey to attempt to encrypt the provided data * @param decoded data to be encrypted * @return encrypted data upon success; null otherwise */ byte[] encrypt(byte[] decoded) { try { Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, mSharedSecretKey); mHandler.removeCallbacks(mClearSharedSecretKey); mHandler.postDelayed(mClearSharedSecretKey, KEY_LIFESPAN); return cipher.doFinal(decoded); } catch (Exception e) { Log.e(TAG, e.toString()); } if (DBG) { Log.w(TAG, "Encryption Failed, clear key"); } mHandler.removeCallbacks(mClearSharedSecretKey); mSharedSecretKey = null; return null; } /** * Utilize the key set via setSharedSecretKey to attempt to decrypt the provided data * @param encoded data to be decrypted * @return decrypted data upon success; null otherwise */ byte[] decrypt(byte[] encoded) { try { Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, mSharedSecretKey); mHandler.removeCallbacks(mClearSharedSecretKey); mHandler.postDelayed(mClearSharedSecretKey, KEY_LIFESPAN); return cipher.doFinal(encoded); } catch (Exception e) { Log.e(TAG, e.toString()); } mHandler.removeCallbacks(mClearSharedSecretKey); mSharedSecretKey = null; return null; } /** * The final step of the Fast Pair procedure involves receiving an account key from the * Fast Pair seeker, authenticating it, and then storing it for future use. * @param accountKey */ void processAccountKey(byte[] accountKey) { byte[] decodedAccountKey = decrypt(accountKey); if (decodedAccountKey != null && decodedAccountKey[0] == 0x04) { if (DBG) { Log.d(TAG, "ReceivedAccountKey" + decodedAccountKey[0]); } FastPairUtils.AccountKey receivedKey = new FastPairUtils.AccountKey(decodedAccountKey); if (!mKeys.contains(receivedKey)) { mKeys.add(receivedKey); } // due to space restrictions in the protocol we can only store 10 keys while (mKeys.size() > MAX_KEY_COUNT) { mKeys.remove(0); } FastPairUtils.writeStoredAccountKeys(mContext, mKeys); mSuccessCount++; } else { if (DBG) { Log.d(TAG, "Invalid Account Key"); } } } /** * New pairings based upon model ID requires the Fast Pair provider to authenticate to the * seeker that it is in possession of the private key associated with the model ID advertised, * this is accomplished via Eliptic-curve Diffie-Hellman * @param localPrivateKey * @param remotePublicKey * @return */ AccountKey calculateAntiSpoofing(byte[] localPrivateKey, byte[] remotePublicKey) { try { if (DBG) { Log.d(TAG, "Calculating secret key from remotePublicKey"); } // Initialize the EC key generator KeyFactory keyFactory = KeyFactory.getInstance("EC"); KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); ECParameterSpec ecParameterSpec = ((ECPublicKey) kpg.generateKeyPair().getPublic()) .getParams(); // Use the private anti-spoofing key ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec( new BigInteger(1, localPrivateKey), ecParameterSpec); // Calculate the public point utilizing the data received from the remote device ECPoint publicPoint = new ECPoint(new BigInteger(1, Arrays.copyOf(remotePublicKey, 32)), new BigInteger(1, Arrays.copyOfRange(remotePublicKey, 32, 64))); ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(publicPoint, ecParameterSpec); PrivateKey privateKey = keyFactory.generatePrivate(ecPrivateKeySpec); PublicKey publicKey = keyFactory.generatePublic(ecPublicKeySpec); // Generate a shared secret KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); keyAgreement.init(privateKey); keyAgreement.doPhase(publicKey, true); byte[] sharedSecret = keyAgreement.generateSecret(); // Use the first 16 bytes of a hash of the shared secret as the session key final byte[] digest = MessageDigest.getInstance("SHA-256").digest(sharedSecret); byte[] AESAntiSpoofingKey = Arrays.copyOf(digest, 16); if (DBG) { Log.d(TAG, "Key calculated"); } return new AccountKey(AESAntiSpoofingKey); } catch (Exception e) { Log.w(TAG, e.toString()); return null; } } /** * Determine if this pairing request is based on the anti-spoof keys associated with the model * id or stored account keys. * @param accountKey * @return */ boolean processKeyBasedPairing(byte[] pairingRequest) { if (mFailureCount >= 10) return false; List possibleKeys = new ArrayList<>(); if (pairingRequest.length == 80) { // if the pairingRequest is 80 bytes long try the anit-spoof key final byte[] remotePublicKey = Arrays.copyOfRange(pairingRequest, 16, 80); possibleKeys .add(calculateAntiSpoofing(Base64.decode(mPrivateAntiSpoof, 0), remotePublicKey) .getKeySpec()); } else { // otherwise the pairing request is the encrypted request, try all the stored account // keys List storedAccountKeys = FastPairUtils.readStoredAccountKeys(mContext); for (AccountKey key : storedAccountKeys) { possibleKeys.add(new SecretKeySpec(key.toBytes(), "AES")); } } byte[] encryptedRequest = Arrays.copyOfRange(pairingRequest, 0, 16); if (DBG) { Log.d(TAG, "Checking " + possibleKeys.size() + " Keys"); } // check all the keys for a valid pairing request for (SecretKeySpec key : possibleKeys) { if (DBG) { Log.d(TAG, "Checking possibleKey"); } if (validatePairingRequest(encryptedRequest, key)) { return true; } } Log.w(TAG, "No Matching Key found"); mFailureCount++; mSharedSecretKey = null; return false; } /** * Check if the pairing request is a valid request. * A request is valid if its decrypted value is of type 0x00 or 0x10 and it contains either the * seekers public or current BLE address * @param encryptedRequest the request to decrypt and validate * @param secretKeySpec the key to use while attempting to decrypt the request * @return true if the key matches, false otherwise */ boolean validatePairingRequest(byte[] encryptedRequest, SecretKeySpec secretKeySpec) { // Decrypt the request mSharedSecretKey = secretKeySpec; byte[] decryptedRequest = decrypt(encryptedRequest); if (decryptedRequest == null) { return false; } if (DBG) { Log.d(TAG, "Decrypted" + decryptedRequest[0] + "Flags" + decryptedRequest[1]); } // Check that the request is either a Key-based Pairing Request or an Action Request if (decryptedRequest[0] == 0 || decryptedRequest[0] == 0x10) { String localAddress = BluetoothAdapter.getDefaultAdapter().getAddress(); byte[] localAddressBytes = FastPairUtils.getBytesFromAddress(localAddress); // Extract the remote address bytes from the message byte[] remoteAddressBytes = Arrays.copyOfRange(decryptedRequest, 2, 8); BluetoothDevice localDevice = BluetoothAdapter.getDefaultAdapter() .getRemoteDevice(localAddress); BluetoothDevice reportedDevice = BluetoothAdapter.getDefaultAdapter() .getRemoteDevice(remoteAddressBytes); if (DBG) { Log.d(TAG, "Local RPA = " + mLocalRpaDevice); Log.d(TAG, "Decrypted, LocalMacAddress: " + localAddress + " remoteAddress: " + reportedDevice.toString()); } // Test that the received device address matches this devices address if (reportedDevice.equals(localDevice) || reportedDevice.equals(mLocalRpaDevice)) { if (DBG) { Log.d(TAG, "SecretKey Validated"); } // encrypt and respond to the seeker with the local public address byte[] rawResponse = new byte[16]; new Random().nextBytes(rawResponse); rawResponse[0] = 0x01; System.arraycopy(localAddressBytes, 0, rawResponse, 1, 6); mEncryptedResponse = encrypt(rawResponse); return encryptedRequest != null; } } return false; } /** * Extract the 6 digit Bluetooth Simple Secure Passkey from the received message and confirm * it matches the key received through the Bluetooth pairing procedure. * If the passkeys match and automatic passkey confirmation is enabled, approve of the pairing. * If the passkeys do not match reject the pairing. * @param pairingKey * @return true if the procedure completed, although pairing may not have been approved */ boolean processPairingKey(byte[] pairingKey) { byte[] decryptedRequest = decrypt(pairingKey); if (decryptedRequest == null) { return false; } int passkey = Byte.toUnsignedInt(decryptedRequest[1]) * 65536 + Byte.toUnsignedInt(decryptedRequest[2]) * 256 + Byte.toUnsignedInt(decryptedRequest[3]); if (DBG) { Log.d(TAG, "PairingKey , MessageType " + decryptedRequest[0] + "FastPair Passkey = " + passkey + "Bluetooth Passkey = " + mPairingPasskey); } // compare the Bluetooth received passkey with the Fast Pair received passkey if (mPairingPasskey == passkey) { if (mAutomaticPasskeyConfirmation) { if (DBG) { Log.d(TAG, "Passkeys match, accepting"); } mRemotePairingDevice.setPairingConfirmation(true); } } else if (mPairingPasskey != INVALID) { Log.w(TAG, "Passkeys don't match, rejecting"); mRemotePairingDevice.setPairingConfirmation(false); } return true; } void sendPairingResponse(int passkey) { if (!isConnected()) return; if (DBG) { Log.d(TAG, "sendPairingResponse + " + passkey); } // Send an encrypted response to the seeker with the Bluetooth passkey as required byte[] decryptedResponse = new byte[16]; new Random().nextBytes(decryptedResponse); ByteBuffer pairingPasskeyBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt( passkey); decryptedResponse[0] = 0x3; decryptedResponse[1] = pairingPasskeyBytes.get(1); decryptedResponse[2] = pairingPasskeyBytes.get(2); decryptedResponse[3] = pairingPasskeyBytes.get(3); mEncryptedResponse = encrypt(decryptedResponse); if (mEncryptedResponse == null) { return; } mPasskeyCharacteristic.setValue(mEncryptedResponse); mBluetoothGattServer .notifyCharacteristicChanged(mRemoteGattDevice, mPasskeyCharacteristic, false); } /** * Initialize all of the GATT characteristics with appropriate default values and the required * configurations. */ void setup() { IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST); mContext.registerReceiver(mPairingAttemptsReceiver, filter); mModelIdCharacteristic = new BluetoothGattCharacteristic(FAST_PAIR_MODEL_ID_UUID.getUuid(), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ); mModelIdCharacteristic.setValue(mModelId); mFastPairService.addCharacteristic(mModelIdCharacteristic); mKeyBasedPairingCharacteristic = new BluetoothGattCharacteristic(KEY_BASED_PAIRING_UUID.getUuid(), BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_WRITE); mKeyBasedPairingCharacteristic.setValue(mModelId); mKeyBasedPairingCharacteristic.addDescriptor(new BluetoothGattDescriptor( CLIENT_CHARACTERISTIC_CONFIG.getUuid(), BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE)); mFastPairService.addCharacteristic(mKeyBasedPairingCharacteristic); mPasskeyCharacteristic = new BluetoothGattCharacteristic(PASSKEY_UUID.getUuid(), BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_WRITE); mPasskeyCharacteristic.setValue(mModelId); mPasskeyCharacteristic.addDescriptor(new BluetoothGattDescriptor( CLIENT_CHARACTERISTIC_CONFIG.getUuid(), BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE)); mFastPairService.addCharacteristic(mPasskeyCharacteristic); mAccountKeyCharacteristic = new BluetoothGattCharacteristic(ACCOUNT_KEY_UUID.getUuid(), BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PERMISSION_WRITE); mFastPairService.addCharacteristic(mAccountKeyCharacteristic); mDeviceNameCharacteristic = new BluetoothGattCharacteristic(DEVICE_NAME_CHARACTERISTIC_CONFIG.getUuid(), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ); mDeviceNameCharacteristic.setValue(BluetoothAdapter.getDefaultAdapter().getName()); mFastPairService.addCharacteristic(mDeviceNameCharacteristic); } void start() { if (mBluetoothGattServer == null) { return; } mBluetoothGattServer.addService(mFastPairService); } void stop() { if (mBluetoothGattServer == null) { return; } mBluetoothGattServer.removeService(mFastPairService); } void dump(IndentingPrintWriter writer) { writer.println("Currently connected to : " + mRemoteGattDevice); writer.println("Successful pairing attempts : " + mSuccessCount); writer.println("Unsuccessful pairing attempts : " + mFailureCount); } }