/* * 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.FastPairAccountKeyStorage.AccountKey; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 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.bluetooth.BluetoothProfile; import android.car.builtin.util.Slogf; 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.Log; import com.android.car.CarLog; import com.android.car.CarServiceUtils; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.util.IndentingPrintWriter; 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.Objects; 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. */ public 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 = CarLog.tagFor(FastPairGattServer.class); private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG); private static final int KEY_LIFESPAN_AWAIT_PAIRING = 60_000; // Spec *does* say indefinitely but not having a timeout is risky. This matches the BT stack's // internal pairing timeout private static final int KEY_LIFESPAN_PAIRING = 35_000; private static final int KEY_LIFESPAN_AWAIT_ACCOUNT_KEY = 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 final FastPairAccountKeyStorage mFastPairAccountKeyStorage; private BluetoothGattServer mBluetoothGattServer; private final BluetoothManager mBluetoothManager; private final BluetoothAdapter mBluetoothAdapter; private final Object mPasskeyLock = new Object(); private int mSeekerPasskey = INVALID; private int mPairingPasskey = INVALID; private final DecryptionFailureCounter mFailureCounter = new DecryptionFailureCounter(); private BluetoothGattService mFastPairService = new BluetoothGattService( FAST_PAIR_SERVICE_UUID.getUuid(), BluetoothGattService.SERVICE_TYPE_PRIMARY); private Callbacks mCallbacks; private SecretKeySpec mSharedSecretKey; 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); } private class DecryptionFailureCounter { public static final int FAILURE_LIMIT = 10; private static final int FAILURE_RESET_TIMEOUT = 300_000; // 5 minutes private int mCount = 0; private Runnable mResetRunnable = new Runnable() { @Override public void run() { Slogf.i(TAG, "Five minutes have expired. Reset failure count to 0"); reset(); } }; public void increment() { if (hasExceededLimit()) { Slogf.w(TAG, "Failure count is already at the limit."); return; } mCount++; Slogf.i(TAG, "Failure count increased, failures=%d", mCount); if (hasExceededLimit()) { Slogf.w(TAG, "Failure count has reached 10, wait 5 minutes for more tries"); mHandler.postDelayed(mResetRunnable, FAILURE_RESET_TIMEOUT); } } public void reset() { Slogf.i(TAG, "Reset failure count"); mHandler.removeCallbacks(mResetRunnable); mCount = 0; } public boolean hasExceededLimit() { return mCount >= FAILURE_LIMIT; } @Override public String toString() { return String.valueOf(mCount); } } /** * Notify this FastPairGattServer of a new RPA from the FastPairAdvertiser */ public void updateLocalRpa(BluetoothDevice device) { mLocalRpaDevice = device; } private Runnable mClearSharedSecretKey = new Runnable() { @Override public void run() { Slogf.w(TAG, "Shared secret key has expired. Clearing key material."); clearSharedSecretKey(); } }; private final Handler mHandler = new Handler( CarServiceUtils.getHandlerThread(FastPairProvider.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) { Slogf.d(TAG, "onConnectionStateChange %d Device: %s", newState, device); } if (newState == BluetoothProfile.STATE_DISCONNECTED) { invalidatePairingPasskeys(); clearSharedSecretKey(); mRemoteGattDevice = null; mRemotePairingDevice = null; mCallbacks.onPairingCompleted(false); } else if (newState == BluetoothProfile.STATE_CONNECTED) { mRemoteGattDevice = device; } } @Override public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { super.onCharacteristicReadRequest(device, requestId, offset, characteristic); if (DBG) { Slogf.d(TAG, "onCharacteristicReadRequest"); } if (characteristic == mModelIdCharacteristic) { if (DBG) { Slogf.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) { Slogf.d(TAG, "onWrite, uuid=%s, length=%d", characteristic.getUuid(), (value != null ? value.length : -1)); } if (characteristic == mKeyBasedPairingCharacteristic) { if (DBG) { Slogf.d(TAG, "onWriteKeyBasedPairingCharacteristic"); } byte[] response = processKeyBasedPairing(value); if (response == null) { Slogf.w(TAG, "Could not process key based pairing request. Ignoring."); mBluetoothGattServer .sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null); return; } mKeyBasedPairingCharacteristic.setValue(response); mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, response); mBluetoothGattServer .notifyCharacteristicChanged(device, mDeviceNameCharacteristic, false); mBluetoothGattServer .notifyCharacteristicChanged(device, mKeyBasedPairingCharacteristic, false); } else if (characteristic == mPasskeyCharacteristic) { if (DBG) { Slogf.d(TAG, "onWritePasskey %s", characteristic.getUuid()); } processPairingKey(value); mBluetoothGattServer .sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null); } else if (characteristic == mAccountKeyCharacteristic) { if (DBG) { Slogf.d(TAG, "onWriteAccountKeyCharacteristic"); } processAccountKey(value); mBluetoothGattServer .sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null); } else { Slogf.w(TAG, "onWriteOther %s", characteristic.getUuid()); } } @Override public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { if (DBG) { Slogf.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) { String action = intent.getAction(); if (DBG) { Slogf.d(TAG, action); } switch (action) { case BluetoothDevice.ACTION_PAIRING_REQUEST: mRemotePairingDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); synchronized (mPasskeyLock) { mPairingPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, INVALID); if (DBG) { Slogf.d(TAG, "Pairing Request - device=%s, pin_code=%s, seeker_passkey=%s", mRemotePairingDevice, mPairingPasskey, mSeekerPasskey); } if (!isConnected()) { Slogf.d(TAG, "Received pairing request outside of a Fast Pair Session"); break; } if (mPairingPasskey == INVALID) { Slogf.w(TAG, "Received an invalid pin_code from the BT stack"); break; } // The Seeker registers for passkey characteristic notifications after // pairing begins on incoming pairings, so we hold our passkey write until // we receive their passkey so we can be sure they have registered for // notifications and will receive our passkey. if (mSeekerPasskey != INVALID) { sendPairingResponse(mPairingPasskey); } else { Slogf.w(TAG, "Got code from BT stack before getting Seeker's passkey"); } if (mSeekerPasskey != INVALID && mPairingPasskey != INVALID) { comparePasskeys(); } } // TODO (243578517): Abort the broadcast when everything is valid and we support // automatic acceptance. break; case BluetoothDevice.ACTION_BOND_STATE_CHANGED: BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, INVALID); int previousState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, INVALID); if (DBG) { Slogf.d(TAG, "Bond State Change - device=%s, old_state=%s, new_state=%s", device, BluetoothUtils.getBondStateName(previousState), BluetoothUtils.getBondStateName(state)); } // If the bond state has changed for the device we're current fast pairing with // and it is now bonded, then pairing is complete. Reset the failure count to 0. // Await a potential account key. if (device != null && device.equals(mRemotePairingDevice)) { if (state == BluetoothDevice.BOND_BONDED) { if (DBG) { Slogf.d(TAG, "Pairing complete, device=%s", mRemotePairingDevice); } setSharedSecretKeyLifespan(KEY_LIFESPAN_AWAIT_ACCOUNT_KEY); mRemotePairingDevice = null; invalidatePairingPasskeys(); mFailureCounter.reset(); } else if (state == BluetoothDevice.BOND_NONE) { if (DBG) { Slogf.d(TAG, "Pairing attempt failed, device=%s", mRemotePairingDevice); } mRemotePairingDevice = null; invalidatePairingPasskeys(); } } break; case BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED: String name = intent.getStringExtra(BluetoothAdapter.EXTRA_LOCAL_NAME); updateLocalName(name); break; default: Slogf.w(TAG, "Unknown action. Skipped"); break; } } }; /** * 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, FastPairAccountKeyStorage fastPairAccountKeyStorage) { mContext = Objects.requireNonNull(context); mFastPairAccountKeyStorage = Objects.requireNonNull(fastPairAccountKeyStorage); mCallbacks = Objects.requireNonNull(callbacks); mPrivateAntiSpoof = antiSpoof; mAutomaticPasskeyConfirmation = automaticAcceptance; mBluetoothManager = context.getSystemService(BluetoothManager.class); mBluetoothAdapter = mBluetoothManager.getAdapter(); ByteBuffer modelIdBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt( modelId); mModelId = Arrays.copyOfRange(modelIdBytes.array(), 0, 3); setup(); } /** * Initialize all of the GATT characteristics with appropriate default values and the required * configurations. */ private void setup() { 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); String name = mBluetoothAdapter.getName(); if (name == null) { name = ""; } mDeviceNameCharacteristic.setValue(name); mFastPairService.addCharacteristic(mDeviceNameCharacteristic); } void updateLocalName(String name) { Slogf.d(TAG, "Device name changed to '%s'", name); if (name != null) { mDeviceNameCharacteristic.setValue(name); } } /** * Start the FastPairGattServer * * This makes the underlying service and characteristics available and registers us for events. */ public synchronized boolean start() { if (DBG) { Slogf.d(TAG, "start()"); } if (isStarted()) { Slogf.w(TAG, "GATT service already started"); return true; } mBluetoothGattServer = mBluetoothManager .openGattServer(mContext, mBluetoothGattServerCallback); if (mBluetoothGattServer == null) { Slogf.e(TAG, "Start failed, could not get a GATT server."); return false; } // Setup filter to receive pairing attempts and passkey. Make this a high priority broadcast // receiver so others can't intercept it before we can handle it. IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST); filter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); mContext.registerReceiver(mPairingAttemptsReceiver, filter); mBluetoothGattServer.addService(mFastPairService); return true; } /** * Stop the FastPairGattServer * * This removes our underlying service and clears our state. */ public synchronized boolean stop() { if (DBG) { Slogf.d(TAG, "stop()"); } if (!isStarted()) { Slogf.w(TAG, "GATT service already stopped"); return true; } mContext.unregisterReceiver(mPairingAttemptsReceiver); if (isConnected()) { mBluetoothGattServer.cancelConnection(mRemoteGattDevice); mRemoteGattDevice = null; mCallbacks.onPairingCompleted(false); } invalidatePairingPasskeys(); clearSharedSecretKey(); mBluetoothGattServer.removeService(mFastPairService); mBluetoothGattServer.close(); mBluetoothGattServer = null; return true; } /** * Check if this service is started */ public boolean isStarted() { return mBluetoothGattServer != null; } /** * Check if a client is connected to this GATT server * @return true if connected; */ public boolean isConnected() { if (DBG) { Slogf.d(TAG, "isConnected() -> %s", (mRemoteGattDevice != null)); } return (mRemoteGattDevice != null); } private void setSharedSecretKey(SecretKeySpec key, int lifespan) { if (key == null) { Slogf.w(TAG, "Cannot set a null shared secret."); return; } Slogf.i(TAG, "Shared secret key set, key=%s lifespan=%d", key, lifespan); mSharedSecretKey = key; setSharedSecretKeyLifespan(lifespan); } private void setSharedSecretKeyLifespan(int lifespan) { if (mSharedSecretKey == null) { Slogf.w(TAG, "Ignoring lifespan on null key"); return; } if (DBG) { Slogf.d(TAG, "Update key lifespan to %d", lifespan); } mHandler.removeCallbacks(mClearSharedSecretKey); if (lifespan > 0) { mHandler.postDelayed(mClearSharedSecretKey, lifespan); } } private void clearSharedSecretKey() { Slogf.i(TAG, "Shared secret key has been cleared"); mHandler.removeCallbacks(mClearSharedSecretKey); mSharedSecretKey = null; } private void invalidatePairingPasskeys() { synchronized (mPasskeyLock) { mPairingPasskey = INVALID; mSeekerPasskey = INVALID; } } public boolean isFastPairSessionActive() { return mSharedSecretKey != null; } /** * Attempt to encrypt the provided data with the provided key * * @param data data to be encrypted * @param secretKeySpec key to ecrypt the data with * @return encrypted data upon success; null otherwise */ private byte[] encrypt(byte[] data, SecretKeySpec secretKeySpec) { if (secretKeySpec == null) { Slogf.e(TAG, "Encryption failed: no key"); return null; } try { Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); return cipher.doFinal(data); } catch (Exception e) { Slogf.e(TAG, "Encryption failed: %s", e); } return null; } /** * Attempt to decrypt the provided data with the provided key * * @param encryptedData data to be decrypted * @param secretKeySpec key to decrypt the data with * @return decrypted data upon success; null otherwise */ private byte[] decrypt(byte[] encryptedData, SecretKeySpec secretKeySpec) { if (secretKeySpec == null) { Slogf.e(TAG, "Decryption failed: no key"); return null; } try { Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); return cipher.doFinal(encryptedData); } catch (Exception e) { Slogf.e(TAG, "Decryption Failed: %s", e); } return null; } /** * Determine if this pairing request is based on the anti-spoof keys associated with the model * id or stored account keys. * * @param pairingRequest Pairing request * @return Whether pairing request is based on the anti-spoof keys associated with the model id * or stored account keys. */ private byte[] processKeyBasedPairing(byte[] pairingRequest) { if (mFailureCounter.hasExceededLimit()) { Slogf.w(TAG, "Failure count has exceeded 10. Ignoring Key-Based Pairing requests"); return null; } if (pairingRequest == null) { Slogf.w(TAG, "Received a null pairing request"); mFailureCounter.increment(); clearSharedSecretKey(); return null; } List possibleKeys = new ArrayList<>(); if (pairingRequest.length == 80) { if (DBG) { Slogf.d(TAG, "Use Anti-spoofing key"); } // 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 if (pairingRequest.length == 16) { if (DBG) { Slogf.d(TAG, "Use stored account keys"); } // otherwise the pairing request is the encrypted request, try all the stored account // keys List storedAccountKeys = mFastPairAccountKeyStorage.getAllAccountKeys(); for (AccountKey key : storedAccountKeys) { possibleKeys.add(new SecretKeySpec(key.toBytes(), "AES")); } } else { Slogf.w(TAG, "Received key based pairing request of invalid length %d", pairingRequest.length); mFailureCounter.increment(); clearSharedSecretKey(); return null; } byte[] encryptedRequest = Arrays.copyOfRange(pairingRequest, 0, 16); if (DBG) { Slogf.d(TAG, "Checking %d Keys", possibleKeys.size()); } // check all the keys for a valid pairing request for (SecretKeySpec key : possibleKeys) { if (DBG) { Slogf.d(TAG, "Checking possible key"); } if (validateRequestAgainstKey(encryptedRequest, key)) { // If the key was able to decrypt the request and the addresses match then set it as // the shared secret and set a lifespan timeout setSharedSecretKey(key, KEY_LIFESPAN_AWAIT_PAIRING); // Use the key to craft encrypted response to the seeker with the local public // address and salt. If encryption goes wrong, move on to the next key String localAddress = mBluetoothAdapter.getAddress(); byte[] localAddressBytes = BluetoothUtils.getBytesFromAddress(localAddress); byte[] rawResponse = new byte[16]; new Random().nextBytes(rawResponse); rawResponse[0] = 0x01; System.arraycopy(localAddressBytes, 0, rawResponse, 1, 6); byte[] response = encrypt(rawResponse, key); if (response == null) { clearSharedSecretKey(); return null; } return response; } } Slogf.w(TAG, "No matching key found"); mFailureCounter.increment(); clearSharedSecretKey(); return null; } /** * New pairings based upon model ID requires the Fast Pair provider to authenticate to that the * seeker 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 */ private AccountKey calculateAntiSpoofing(byte[] localPrivateKey, byte[] remotePublicKey) { try { if (DBG) { Slogf.d(TAG, "Calculating secret key from remote public key"); } // 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) { Slogf.d(TAG, "Key calculated"); } return new AccountKey(AESAntiSpoofingKey); } catch (Exception e) { Slogf.w(TAG, "Error calculating anti-spoofing key: %s", e); return null; } } /** * Check if the given key can be used to decrypt the pairing request and prove the request is * valid. * * 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. If a key successfully decrypts and validates a request * then that is the key we should use as our shared secret key. * * @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 */ private boolean validateRequestAgainstKey(byte[] encryptedRequest, SecretKeySpec secretKeySpec) { // Decrypt the request byte[] decryptedRequest = decrypt(encryptedRequest, secretKeySpec); if (decryptedRequest == null) { return false; } if (DBG) { StringBuilder sb = new StringBuilder(); for (byte b : decryptedRequest) { sb.append(String.format("%02X ", b)); } Slogf.d(TAG, "Decrypted Request=[ %s]", sb.toString()); } // Check that the request is either a Key-based Pairing Request or an Action Request if (decryptedRequest[0] == 0x00 || decryptedRequest[0] == 0x10) { String localAddress = mBluetoothAdapter.getAddress(); // Extract the remote address bytes from the message byte[] remoteAddressBytes = Arrays.copyOfRange(decryptedRequest, 2, 8); BluetoothDevice localDevice = mBluetoothAdapter.getRemoteDevice(localAddress); BluetoothDevice reportedDevice = mBluetoothAdapter.getRemoteDevice(remoteAddressBytes); if (DBG) { Slogf.d(TAG, "rpa=%s, public=%s, reported=%s", mLocalRpaDevice, localAddress, reportedDevice); } if (mLocalRpaDevice == null) { Slogf.w(TAG, "Cannot get own address"); } // Test that the received device address matches this devices address if (reportedDevice.equals(localDevice) || reportedDevice.equals(mLocalRpaDevice)) { if (DBG) { Slogf.d(TAG, "SecretKey Validated"); } 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 and invalidate our key material. * * @param pairingKey * @return true if the procedure completed, although pairing may not have been approved */ private boolean processPairingKey(byte[] pairingKey) { if (pairingKey == null || pairingKey.length != 16) { clearSharedSecretKey(); return false; } byte[] decryptedRequest = decrypt(pairingKey, mSharedSecretKey); if (decryptedRequest == null) { clearSharedSecretKey(); return false; } synchronized (mPasskeyLock) { mSeekerPasskey = Byte.toUnsignedInt(decryptedRequest[1]) * 65536 + Byte.toUnsignedInt(decryptedRequest[2]) * 256 + Byte.toUnsignedInt(decryptedRequest[3]); if (DBG) { Slogf.d(TAG, "Received passkey request, type=%s, passkey=%d, our_passkey=%d", decryptedRequest[0], mSeekerPasskey, mPairingPasskey); } // The Seeker registers for passkey characteristic notifications after pairing begins // on incoming pairings, so we hold our passkey write until we receive their passkey so // we can be sure they have registered for notifications and will receive our passkey. if (mPairingPasskey != INVALID) { sendPairingResponse(mPairingPasskey); } else { if (DBG) { Slogf.d(TAG, "Got Seeker's passkey before receiving pin code from BT stack"); } } if (mSeekerPasskey != INVALID && mPairingPasskey != INVALID) { comparePasskeys(); } } return true; } /** * Compares the BT Stack reported passkey to the Fast Pair Seeker reported passkey. */ private void comparePasskeys() { synchronized (mPasskeyLock) { if (mPairingPasskey == INVALID || mSeekerPasskey == INVALID) { Slogf.w(TAG, "Mising passkey to compare, bt=%s, seeker=%s", mPairingPasskey, mSeekerPasskey); return; } if (mPairingPasskey == mSeekerPasskey) { if (DBG) { Slogf.d(TAG, "Passkeys match, auto_accept=%s", mAutomaticPasskeyConfirmation); } if (mAutomaticPasskeyConfirmation) { mRemotePairingDevice.setPairingConfirmation(true); } } else { Slogf.w(TAG, "Passkeys don't match, rejecting"); mRemotePairingDevice.setPairingConfirmation(false); clearSharedSecretKey(); } } } /** * Send the seeker the pin code we received so they can validate it. Encrypt it with our shared * secret. * * @param passkey the key-based pairing passkey, as described by the core BT specification */ private void sendPairingResponse(int passkey) { if (!isConnected()) return; if (DBG) { Slogf.d(TAG, "sendPairingResponse %d", passkey); } // Once pairing begins, we can hold on to the shared secret key until pairing // completes setSharedSecretKeyLifespan(KEY_LIFESPAN_PAIRING); // 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); byte[] response = encrypt(decryptedResponse, mSharedSecretKey); if (response == null) { clearSharedSecretKey(); return; } mPasskeyCharacteristic.setValue(response); mBluetoothGattServer .notifyCharacteristicChanged(mRemoteGattDevice, mPasskeyCharacteristic, false); } /** * 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. Only one attempt * at writing this key is allowed by the spec. Discard the shared secret after this one attempt. * * @param accountKey the account key, encrypted with our sharded secret */ private void processAccountKey(byte[] accountKey) { if (accountKey == null || accountKey.length != 16) { clearSharedSecretKey(); return; } byte[] decodedAccountKey = decrypt(accountKey, mSharedSecretKey); if (decodedAccountKey != null && decodedAccountKey[0] == 0x04) { AccountKey receivedKey = new AccountKey(decodedAccountKey); if (DBG) { Slogf.d(TAG, "Received Account Key, key=%s", receivedKey); } mFastPairAccountKeyStorage.add(receivedKey); } else { if (DBG) { Slogf.d(TAG, "Received invalid Account Key"); } } // Always clear the shared secret key following any attempt to write an account key clearSharedSecretKey(); } @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) void dump(IndentingPrintWriter writer) { writer.println("FastPairGattServer:"); writer.increaseIndent(); writer.println("Started : " + isStarted()); writer.println("Active : " + isFastPairSessionActive()); writer.println("Currently connected to : " + mRemoteGattDevice); writer.println("Failure counter : " + mFailureCounter); writer.decreaseIndent(); } }