/* * Copyright (C) 2019 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.trust; import android.annotation.Nullable; import android.app.ActivityManager; import android.bluetooth.BluetoothDevice; import android.car.trust.TrustedDeviceInfo; import android.content.Context; import android.content.SharedPreferences; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.sysprop.CarProperties; import android.util.Base64; import android.util.Log; import com.android.car.CarServiceBase; import com.android.car.R; import com.android.car.Utils; import java.io.IOException; import java.io.PrintWriter; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.List; import java.util.UUID; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; /** * The part of the Car service that enables the Trusted device feature. Trusted Device is a feature * where a remote device is enrolled as a trusted device that can authorize an Android user in lieu * of the user entering a password or PIN. *
* It is comprised of the {@link CarTrustAgentEnrollmentService} for handling enrollment and
* {@link CarTrustAgentUnlockService} for handling unlock/auth.
*
*/
public class CarTrustedDeviceService implements CarServiceBase {
private static final String TAG = CarTrustedDeviceService.class.getSimpleName();
private static final String UNIQUE_ID_KEY = "CTABM_unique_id";
private static final String PREF_ENCRYPTION_KEY_PREFIX = "CTABM_encryption_key";
private static final String KEY_ALIAS = "Ukey2Key";
private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
private static final String IV_SPEC_SEPARATOR = ";";
// Device name length is limited by available bytes in BLE advertisement data packet.
//
// BLE advertisement limits data packet length to 31
// Currently we send:
// - 18 bytes for 16 chars UUID: 16 bytes + 2 bytes for header;
// - 3 bytes for advertisement being connectable;
// which leaves 10 bytes.
// Subtracting 2 bytes used by header, we have 8 bytes for device name.
private static final int DEVICE_NAME_LENGTH_LIMIT = 8;
// Limit prefix to 4 chars and fill the rest with randomly generated name. Use random name
// to improve uniqueness in paired device name.
private static final int DEVICE_NAME_PREFIX_LIMIT = 4;
// The length of the authentication tag for a cipher in GCM mode. The GCM specification states
// that this length can only have the values {128, 120, 112, 104, 96}. Using the highest
// possible value.
private static final int GCM_AUTHENTICATION_TAG_LENGTH = 128;
private final Context mContext;
private CarTrustAgentEnrollmentService mCarTrustAgentEnrollmentService;
private CarTrustAgentUnlockService mCarTrustAgentUnlockService;
private CarTrustAgentBleManager mCarTrustAgentBleManager;
private SharedPreferences mTrustAgentTokenPreferences;
private UUID mUniqueId;
private String mEnrollmentDeviceName;
public CarTrustedDeviceService(Context context) {
mContext = context;
// TODO(b/134695083): Decouple these classes. The services should instead register as
// listeners on CarTrustAgentBleManager. CarTrustAgentBleManager should not know about
// the services and just dispatch BLE events.
mCarTrustAgentBleManager = new CarTrustAgentBleManager(context);
mCarTrustAgentEnrollmentService = new CarTrustAgentEnrollmentService(mContext, this,
mCarTrustAgentBleManager);
mCarTrustAgentUnlockService = new CarTrustAgentUnlockService(this,
mCarTrustAgentBleManager);
}
@Override
public synchronized void init() {
mCarTrustAgentEnrollmentService.init();
mCarTrustAgentUnlockService.init();
}
@Override
public synchronized void release() {
mCarTrustAgentBleManager.cleanup();
mCarTrustAgentEnrollmentService.release();
mCarTrustAgentUnlockService.release();
}
/**
* Returns the internal {@link CarTrustAgentEnrollmentService} instance.
*/
public CarTrustAgentEnrollmentService getCarTrustAgentEnrollmentService() {
return mCarTrustAgentEnrollmentService;
}
/**
* Returns the internal {@link CarTrustAgentUnlockService} instance.
*/
public CarTrustAgentUnlockService getCarTrustAgentUnlockService() {
return mCarTrustAgentUnlockService;
}
/**
* Returns User Id for the given token handle
*
* @param handle The handle corresponding to the escrow token
* @return User id corresponding to the handle
*/
int getUserHandleByTokenHandle(long handle) {
return getSharedPrefs().getInt(String.valueOf(handle), -1);
}
void onRemoteDeviceConnected(BluetoothDevice device) {
mCarTrustAgentEnrollmentService.onRemoteDeviceConnected(device);
mCarTrustAgentUnlockService.onRemoteDeviceConnected(device);
}
void onRemoteDeviceDisconnected(BluetoothDevice device) {
mCarTrustAgentEnrollmentService.onRemoteDeviceDisconnected(device);
mCarTrustAgentUnlockService.onRemoteDeviceDisconnected(device);
}
void onDeviceNameRetrieved(String deviceName) {
mCarTrustAgentEnrollmentService.onDeviceNameRetrieved(deviceName);
}
void cleanupBleService() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "cleanupBleService");
}
mCarTrustAgentBleManager.stopGattServer();
mCarTrustAgentBleManager.stopEnrollmentAdvertising();
mCarTrustAgentBleManager.stopUnlockAdvertising();
}
SharedPreferences getSharedPrefs() {
if (mTrustAgentTokenPreferences != null) {
return mTrustAgentTokenPreferences;
}
mTrustAgentTokenPreferences = mContext.getSharedPreferences(
mContext.getString(R.string.token_handle_shared_preferences), Context.MODE_PRIVATE);
return mTrustAgentTokenPreferences;
}
@Override
public void dump(PrintWriter writer) {
writer.println("*CarTrustedDeviceService*");
int uid = ActivityManager.getCurrentUser();
writer.println("current user id: " + uid);
List The returned name will be a combination of a prefix sysprop and randomized digits.
*/
String getEnrollmentDeviceName() {
if (mEnrollmentDeviceName == null) {
String deviceNamePrefix =
CarProperties.trusted_device_device_name_prefix().orElse("");
deviceNamePrefix = deviceNamePrefix.substring(
0, Math.min(deviceNamePrefix.length(), DEVICE_NAME_PREFIX_LIMIT));
int randomNameLength = DEVICE_NAME_LENGTH_LIMIT - deviceNamePrefix.length();
String randomName = Utils.generateRandomNumberString(randomNameLength);
mEnrollmentDeviceName = deviceNamePrefix + randomName;
}
return mEnrollmentDeviceName;
}
/**
* Encrypt value with designated key
*
* The encrypted value is of the form:
*
* key + IV_SPEC_SEPARATOR + ivSpec
*
* The {@code ivSpec} is needed to decrypt this key later on.
*
* @param keyAlias KeyStore alias for key to use
* @param value a value to encrypt
* @return encrypted value, null if unable to encrypt
*/
@Nullable
String encryptWithKeyStore(String keyAlias, byte[] value) {
if (value == null) {
return null;
}
Key key = getKeyStoreKey(keyAlias);
try {
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, key);
return new StringBuffer(Base64.encodeToString(cipher.doFinal(value), Base64.DEFAULT))
.append(IV_SPEC_SEPARATOR)
.append(Base64.encodeToString(cipher.getIV(), Base64.DEFAULT))
.toString();
} catch (IllegalBlockSizeException
| BadPaddingException
| NoSuchAlgorithmException
| NoSuchPaddingException
| IllegalStateException
| InvalidKeyException e) {
Log.e(TAG, "Unable to encrypt value with key " + keyAlias, e);
return null;
}
}
/**
* Decrypt value with designated key
*
* @param keyAlias KeyStore alias for key to use
* @param value encrypted value
* @return decrypted value, null if unable to decrypt
*/
@Nullable
byte[] decryptWithKeyStore(String keyAlias, byte[] value, byte[] ivSpec) {
if (value == null) {
return null;
}
try {
Key key = getKeyStoreKey(keyAlias);
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, key,
new GCMParameterSpec(GCM_AUTHENTICATION_TAG_LENGTH, ivSpec));
return cipher.doFinal(value);
} catch (IllegalBlockSizeException
| BadPaddingException
| NoSuchAlgorithmException
| NoSuchPaddingException
| IllegalStateException
| InvalidKeyException
| InvalidAlgorithmParameterException e) {
Log.e(TAG, "Unable to decrypt value with key " + keyAlias, e);
return null;
}
}
private Key getKeyStoreKey(String keyAlias) {
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
keyStore.load(null);
if (!keyStore.containsAlias(keyAlias)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER);
keyGenerator.init(
new KeyGenParameterSpec.Builder(keyAlias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build());
keyGenerator.generateKey();
}
return keyStore.getKey(keyAlias, null);
} catch (KeyStoreException
| NoSuchAlgorithmException
| UnrecoverableKeyException
| NoSuchProviderException
| CertificateException
| IOException
| InvalidAlgorithmParameterException e) {
Log.e(TAG, "Unable to retrieve key " + keyAlias + " from KeyStore.", e);
throw new IllegalStateException(e);
}
}
}