diff --git a/core/api/system-current.txt b/core/api/system-current.txt index c0df87a641bb3..fcbbc6aa9bf3c 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -411,6 +411,7 @@ package android { field public static final String RECORD_BACKGROUND_AUDIO = "android.permission.RECORD_BACKGROUND_AUDIO"; field public static final String RECOVERY = "android.permission.RECOVERY"; field public static final String RECOVER_KEYSTORE = "android.permission.RECOVER_KEYSTORE"; + field public static final String RECOVER_KEYSTORE_GMSCORE = "android.permission.RECOVER_KEYSTORE_GMSCORE"; field public static final String REGISTER_CALL_PROVIDER = "android.permission.REGISTER_CALL_PROVIDER"; field public static final String REGISTER_CONNECTION_MANAGER = "android.permission.REGISTER_CONNECTION_MANAGER"; field @FlaggedApi("android.net.platform.flags.register_nsd_offload_engine") public static final String REGISTER_NSD_OFFLOAD_ENGINE = "android.permission.REGISTER_NSD_OFFLOAD_ENGINE"; @@ -5821,6 +5822,7 @@ package android.ext { } public interface KnownSystemPackage { + field public static final int GMS_COMPAT = 4; // 0x4 field public static final int SETTINGS = 0; // 0x0 field public static final int SETUP_WIZARD = 3; // 0x3 field public static final int SHELL = 1; // 0x1 @@ -5831,6 +5833,7 @@ package android.ext { method @NonNull public static android.ext.KnownSystemPackages get(@NonNull android.content.Context); method @NonNull public String getById(int); field @NonNull public final String contactsProvider; + field @NonNull public final String gmsCompat; field @NonNull public final String launcher; field @NonNull public final String mediaProvider; field @NonNull public final String permissionController; diff --git a/core/java/android/app/compat/gms/GmsCorePackageFlag.java b/core/java/android/app/compat/gms/GmsCorePackageFlag.java index e8ffcb3140ae6..060f0daa44849 100644 --- a/core/java/android/app/compat/gms/GmsCorePackageFlag.java +++ b/core/java/android/app/compat/gms/GmsCorePackageFlag.java @@ -3,4 +3,5 @@ /** @hide */ public interface GmsCorePackageFlag { int GRANT_PERMS_FOR_ICC_AUTHENTICATION = 0; + int GRANT_PERMS_FOR_RECOVER_KEYSTORE_GMSCORE = 1; } diff --git a/core/java/android/ext/KnownSystemPackage.java b/core/java/android/ext/KnownSystemPackage.java index bb88a92edad51..fd5d91d54feca 100644 --- a/core/java/android/ext/KnownSystemPackage.java +++ b/core/java/android/ext/KnownSystemPackage.java @@ -13,6 +13,7 @@ public interface KnownSystemPackage { int SHELL = 1; int SYSTEM_UI = 2; int SETUP_WIZARD = 3; + int GMS_COMPAT = 4; /** @hide */ @IntDef(value = { @@ -20,6 +21,7 @@ public interface KnownSystemPackage { SHELL, SYSTEM_UI, SETUP_WIZARD, + GMS_COMPAT, }) @Retention(RetentionPolicy.SOURCE) @interface Enum {} diff --git a/core/java/android/ext/KnownSystemPackages.java b/core/java/android/ext/KnownSystemPackages.java index 839f04c0773bc..83d162167c8fe 100644 --- a/core/java/android/ext/KnownSystemPackages.java +++ b/core/java/android/ext/KnownSystemPackages.java @@ -22,6 +22,7 @@ public static KnownSystemPackages get(@NonNull Context ctx) { } @NonNull public final String contactsProvider; + @NonNull public final String gmsCompat; @NonNull public final String launcher; @NonNull public final String mediaProvider; @NonNull public final String permissionController; @@ -33,6 +34,7 @@ public static KnownSystemPackages get(@NonNull Context ctx) { private KnownSystemPackages(Context ctx) { Resources res = ctx.getResources(); contactsProvider = "com.android.providers.contacts"; + gmsCompat = "app.grapheneos.gmscompat"; launcher = "com.android.launcher3"; mediaProvider = "com.android.providers.media.module"; permissionController = "com.android.permissioncontroller"; @@ -49,6 +51,7 @@ public String getById(@KnownSystemPackage.Enum int id) { case KnownSystemPackage.SHELL -> shell; case KnownSystemPackage.SYSTEM_UI -> systemUi; case KnownSystemPackage.SETUP_WIZARD -> setupWizard; + case KnownSystemPackage.GMS_COMPAT -> gmsCompat; default -> throw new IllegalArgumentException(); }; } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index bcf423537bb66..b8bb77cd32c7b 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14470,7 +14470,7 @@ public static final class Global extends NameValueTable { public static final String WIFI_AUTO_OFF = "wifi_off_timeout"; /** @hide */ - @Protected(readWrite = KnownSystemPackage.SETTINGS) + @Protected(read = KnownSystemPackage.GMS_COMPAT, readWrite = KnownSystemPackage.SETTINGS) public static final String BLUETOOTH_AUTO_OFF = "bluetooth_off_timeout"; /** @hide */ diff --git a/core/java/com/android/internal/gmscompat/GmsHooks.java b/core/java/com/android/internal/gmscompat/GmsHooks.java index f0c8d248f5ec2..41a72bada8a1b 100644 --- a/core/java/com/android/internal/gmscompat/GmsHooks.java +++ b/core/java/com/android/internal/gmscompat/GmsHooks.java @@ -17,6 +17,7 @@ package com.android.internal.gmscompat; import android.Manifest; +import android.accounts.AccountManager; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.Activity; @@ -34,11 +35,13 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.credentials.CredentialOption; import android.database.Cursor; import android.database.MatrixCursor; import android.database.sqlite.SQLiteOpenHelper; import android.ext.PackageId; import android.net.Uri; +import android.os.BadParcelableException; import android.os.Binder; import android.os.Bundle; import android.os.DeadSystemRuntimeException; @@ -51,6 +54,9 @@ import android.os.UserHandle; import android.provider.Downloads; import android.provider.Settings; +import android.service.credentials.CreateCredentialRequest; +import android.service.credentials.CredentialProviderService; +import android.service.credentials.GetCredentialRequest; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -489,6 +495,149 @@ public static void activityOnCreate(Activity activity) { Log.i(TAG, "calling setTranslucent(false) for " + activity.getClass().getName()); activity.setTranslucent(false); } + + maybeShowRecoverableKeystorePermissionNotification(activity.getIntent()); + } + + private static void maybeShowRecoverableKeystorePermissionNotification( + @Nullable Intent intent) { + if (!GmsCompat.isGmsCore() + || GmsCompat.hasPermission(Manifest.permission.RECOVER_KEYSTORE_GMSCORE)) { + return; + } + + if (isPublicKeyCredentialRequest(intent)) { + if (!hasGoogleAccount()) { + return; + } + try { + GmsCompatApp.iGms2Gca() + .maybeShowGmsCoreMissingRecoverableKeystorePermissionNotification(); + } catch (RemoteException e) { + Log.w(TAG, "failed to show passkey account keychain notification", e); + } + return; + } + + if (!isFindHubProvisioningIntent(intent)) { + return; + } + + try { + GmsCompatApp.iGms2Gca() + .maybeShowGmsCoreMissingFindHubAccountKeychainPermissionNotification(); + } catch (RemoteException e) { + Log.w(TAG, "failed to show Find Hub account keychain notification", e); + } + } + + private static final String TYPE_PUBLIC_KEY_CREDENTIAL = + "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"; + private static final String FIDO_REGISTRATION_ACTIVITY = + "com.google.android.gms.auth.api.credentials.fido.registration.ui." + + "RegistrationActivity"; + private static final String FIDO_AUTHENTICATION_ACTIVITY = + "com.google.android.gms.auth.api.credentials.fido.authentication.ui." + + "AuthenticationActivity"; + private static final String ACTION_FIDO_REGISTRATION = + "com.google.android.gms.auth.api.credentials.FIDO_REGISTRATION"; + private static final String ACTION_FIDO_AUTHENTICATION = + "com.google.android.gms.auth.api.credentials.FIDO_AUTHENTICATION"; + private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; + private static final String FIND_HUB_PROVISION_ACTIVITY = + "com.google.android.gms.findmydevice.spot.fastpair.halfsheet.ProvisionActivity"; + private static final String FAST_PAIR_HALF_SHEET_ACTIVITY = + "com.google.android.gms.nearby.discovery.fastpair.HalfSheetActivity"; + private static final String EXTRA_HALF_SHEET_TYPE = + "com.google.android.gms.nearby.discovery.fastpair.EXTRA_HALF_SHEET_TYPE"; + private static final String HALF_SHEET_TYPE_SPOT = "SPOT"; + + private static boolean hasGoogleAccount() { + try { + return AccountManager.get(GmsCompat.appContext()) + .getAccountsByType(GOOGLE_ACCOUNT_TYPE).length != 0; + } catch (RuntimeException e) { + // Skip the optional notification if the account query fails. + Log.w(TAG, "failed to query Google accounts", e); + return false; + } + } + + private static boolean isPublicKeyCredentialRequest(@Nullable Intent intent) { + if (intent == null) { + return false; + } + + if (isGmsCoreFidoIntent(intent)) { + return true; + } + + try { + CreateCredentialRequest createRequest = intent.getParcelableExtra( + CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST, + CreateCredentialRequest.class); + if (createRequest != null + && TYPE_PUBLIC_KEY_CREDENTIAL.equals(createRequest.getType())) { + return true; + } + + GetCredentialRequest getRequest = intent.getParcelableExtra( + CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST, + GetCredentialRequest.class); + if (getRequest != null) { + for (CredentialOption option : getRequest.getCredentialOptions()) { + if (option != null + && TYPE_PUBLIC_KEY_CREDENTIAL.equals(option.getType())) { + return true; + } + } + } + } catch (BadParcelableException e) { + Log.w(TAG, "failed to inspect Credential Manager request", e); + } + + return false; + } + + private static boolean isGmsCoreFidoIntent(Intent intent) { + ComponentName component = intent.getComponent(); + if (component == null || !PACKAGE_GMS_CORE.equals(component.getPackageName())) { + return false; + } + + String className = component.getClassName(); + String action = intent.getAction(); + return (FIDO_REGISTRATION_ACTIVITY.equals(className) + && ACTION_FIDO_REGISTRATION.equals(action)) + || (FIDO_AUTHENTICATION_ACTIVITY.equals(className) + && ACTION_FIDO_AUTHENTICATION.equals(action)); + } + + private static boolean isFindHubProvisioningIntent(@Nullable Intent intent) { + if (intent == null) { + return false; + } + + ComponentName component = intent.getComponent(); + if (component == null || !PACKAGE_GMS_CORE.equals(component.getPackageName())) { + return false; + } + + String className = component.getClassName(); + if (FIND_HUB_PROVISION_ACTIVITY.equals(className)) { + return true; + } + + if (!FAST_PAIR_HALF_SHEET_ACTIVITY.equals(className)) { + return false; + } + + try { + return HALF_SHEET_TYPE_SPOT.equals(intent.getStringExtra(EXTRA_HALF_SHEET_TYPE)); + } catch (BadParcelableException e) { + Log.w(TAG, "failed to inspect Fast Pair half sheet type", e); + return false; + } } // ContentResolver#insert(Uri, ContentValues, Bundle) diff --git a/core/java/com/android/internal/gmscompat/IGms2Gca.aidl b/core/java/com/android/internal/gmscompat/IGms2Gca.aidl index f44173479d5e1..8e217a252f7d5 100644 --- a/core/java/com/android/internal/gmscompat/IGms2Gca.aidl +++ b/core/java/com/android/internal/gmscompat/IGms2Gca.aidl @@ -30,6 +30,10 @@ interface IGms2Gca { oneway void showGmsCoreMissingNearbyDevicesPermissionGeneric(); + oneway void maybeShowGmsCoreMissingRecoverableKeystorePermissionNotification(); + + oneway void maybeShowGmsCoreMissingFindHubAccountKeychainPermissionNotification(); + oneway void showMissingPostNotifsPermissionNotification(String callerPkg); oneway void maybeShowContactsSyncNotification(); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index fa2c3f0534c0d..e377711f2c775 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7959,6 +7959,10 @@ + + + diff --git a/services/core/java/com/android/server/locksettings/LockSettingsInternal.java b/services/core/java/com/android/server/locksettings/LockSettingsInternal.java index 67224496e48e4..a3de4df6af75e 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsInternal.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsInternal.java @@ -80,6 +80,15 @@ public abstract class LockSettingsInternal { */ public abstract void removeUser(@UserIdInt int userId); + /** + * Removes recoverable keystore state for the given recovery agent. + * + * @param userId the ID of the user whose state is being removed + * @param uid the UID of the recovery agent + */ + public abstract void removeRecoverableKeystoreStateForRecoveryAgent( + @UserIdInt int userId, int uid); + /** * Create an escrow token for the current user, which can later be used to unlock FBE * or change user password. diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index adfeeab98043b..a0cbe2f53916a 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -4358,6 +4358,13 @@ public void removeUser(@UserIdInt int userId) { LockSettingsService.this.removeUser(userId); } + @Override + public void removeRecoverableKeystoreStateForRecoveryAgent( + @UserIdInt int userId, int uid) { + mRecoverableKeyStoreManager.removeRecoverableKeystoreStateForRecoveryAgent( + userId, uid); + } + @Override public long addEscrowToken(byte[] token, int userId, EscrowTokenStateChangeCallback callback) { diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java index 81031770ff680..dbdedf300c6bc 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java @@ -21,6 +21,7 @@ import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; +import java.util.Enumeration; /** * Proxies {@link java.security.KeyStore}. As all of its methods are final, it cannot otherwise be @@ -43,4 +44,7 @@ void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter p /** @see KeyStore#deleteEntry(String) */ void deleteEntry(String alias) throws KeyStoreException; + + /** @see KeyStore#aliases() */ + Enumeration aliases() throws KeyStoreException; } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java index f5941361bd897..603a439f88f9f 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java @@ -23,6 +23,7 @@ import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; +import java.util.Enumeration; /** * Implementation of {@link KeyStoreProxy} that delegates all method calls to the {@link KeyStore}. @@ -62,6 +63,11 @@ public void deleteEntry(String alias) throws KeyStoreException { mKeyStore.deleteEntry(alias); } + @Override + public Enumeration aliases() throws KeyStoreException { + return mKeyStore.aliases(); + } + /** * Returns AndroidKeyStore-provided {@link KeyStore}, having already invoked * {@link KeyStore#load(KeyStore.LoadStoreParameter)}. diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java index 5279219c29285..a0180c0f272c2 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java @@ -91,6 +91,7 @@ public class KeySyncTask implements Runnable { private final RecoverySnapshotListenersStorage mSnapshotListenersStorage; private final TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper; private final Scrypt mScrypt; + private boolean mSensitiveCryptoStarted; public static KeySyncTask newInstance( Context context, @@ -158,6 +159,12 @@ public void run() { Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e); } finally { mCredential.zeroize(); // no longer needed + if (mSensitiveCryptoStarted) { + // Best effort cleanup for unreachable sensitive objects and native contexts. + System.gc(); + System.runFinalization(); + System.gc(); + } } } @@ -274,12 +281,9 @@ private void syncKeysForAgent(int recoveryAgentUid) throws IOException, RemoteEx boolean useScryptToHashCredential = shouldUseScryptToHashCredential(); byte[] salt = generateNewSaltIfNecessary(); - byte[] localLskfHash; - if (useScryptToHashCredential) { - localLskfHash = hashCredentialsByScrypt(salt, mCredential.getCredential()); - } else { - localLskfHash = hashCredentialsBySaltedSha256(salt, mCredential.getCredential()); - } + + // Loading keys can create movable plaintext key material before reporting a failure. + mSensitiveCryptoStarted = true; Map> rawKeysWithMetadata; try { @@ -343,10 +347,18 @@ private void syncKeysForAgent(int recoveryAgentUid) throws IOException, RemoteEx vaultHandle); byte[] encryptedRecoveryKey; + // Derive the LSKF hash immediately before its only use so it stays in memory for less time. + byte[] localLskfHash_ = null; try { + if (useScryptToHashCredential) { + localLskfHash_ = hashCredentialsByScrypt(salt, mCredential.getCredential()); + } else { + localLskfHash_ = + hashCredentialsBySaltedSha256(salt, mCredential.getCredential()); + } encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey( publicKey, - localLskfHash, + localLskfHash_, vaultParams, recoveryKey); } catch (NoSuchAlgorithmException e) { @@ -355,6 +367,8 @@ private void syncKeysForAgent(int recoveryAgentUid) throws IOException, RemoteEx } catch (InvalidKeyException e) { Log.e(TAG,"Could not encrypt with recovery key", e); return; + } finally { + ArrayUtils.zeroize(localLskfHash_); } KeyDerivationParams keyDerivationParams; diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java index 710e9c5dcc1b5..088717bd4c5ac 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java @@ -34,6 +34,7 @@ import android.app.RemoteLockscreenValidationResult; import android.app.RemoteLockscreenValidationSession; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Binder; import android.os.RemoteException; import android.os.ServiceSpecificException; @@ -89,6 +90,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -219,7 +221,7 @@ public class RecoverableKeyStoreManager { void initRecoveryService( @NonNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); int userId = UserHandle.getCallingUserId(); int uid = Binder.getCallingUid(); @@ -334,7 +336,7 @@ public void initRecoveryServiceWithSigFile( @NonNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile, @NonNull byte[] recoveryServiceSigFile) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); rootCertificateAlias = mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias); Objects.requireNonNull(recoveryServiceCertFile, "recoveryServiceCertFile is null"); @@ -373,7 +375,7 @@ public void initRecoveryServiceWithSigFile( */ public @NonNull KeyChainSnapshot getKeyChainSnapshot() throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); int uid = Binder.getCallingUid(); KeyChainSnapshot snapshot = mSnapshotStorage.get(uid); if (snapshot == null) { @@ -384,7 +386,7 @@ public void initRecoveryServiceWithSigFile( public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); int uid = Binder.getCallingUid(); mListenersStorage.setSnapshotListener(uid, intent); } @@ -394,7 +396,7 @@ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent) * chain. Along with the counter ID, it is used to uniquely identify an instance of a vault. */ public void setServerParams(@NonNull byte[] serverParams) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); int userId = UserHandle.getCallingUserId(); int uid = Binder.getCallingUid(); @@ -428,7 +430,7 @@ public void setServerParams(@NonNull byte[] serverParams) throws RemoteException * Sets the recovery status of key with {@code alias} to {@code status}. */ public void setRecoveryStatus(@NonNull String alias, int status) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); Objects.requireNonNull(alias, "alias is null"); long updatedRows = mDatabase.setRecoveryStatus(Binder.getCallingUid(), alias, status); if (updatedRows < 0) { @@ -447,7 +449,7 @@ public void setRecoveryStatus(@NonNull String alias, int status) throws RemoteEx * {@link RecoveryController#RECOVERY_STATUS_PERMANENT_FAILURE}. */ public @NonNull Map getRecoveryStatus() throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); return mDatabase.getStatusForAllKeys(Binder.getCallingUid()); } @@ -459,7 +461,7 @@ public void setRecoveryStatus(@NonNull String alias, int status) throws RemoteEx public void setRecoverySecretTypes( @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); Objects.requireNonNull(secretTypes, "secretTypes is null"); int userId = UserHandle.getCallingUserId(); int uid = Binder.getCallingUid(); @@ -497,7 +499,7 @@ public void setRecoverySecretTypes( * @hide */ public @NonNull int[] getRecoverySecretTypes() throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); return mDatabase.getRecoverySecretTypes(UserHandle.getCallingUserId(), Binder.getCallingUid()); } @@ -524,7 +526,7 @@ public void setRecoverySecretTypes( @NonNull byte[] vaultChallenge, @NonNull List secrets) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); int uid = Binder.getCallingUid(); if (secrets.size() != 1) { @@ -589,7 +591,7 @@ public void setRecoverySecretTypes( @NonNull byte[] vaultChallenge, @NonNull List secrets) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); rootCertificateAlias = mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias); Objects.requireNonNull(sessionId, "invalid session"); @@ -640,7 +642,7 @@ public void setRecoverySecretTypes( @NonNull String sessionId, @NonNull byte[] encryptedRecoveryKey, @NonNull List applicationKeys) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); int userId = UserHandle.getCallingUserId(); int uid = Binder.getCallingUid(); RecoverySessionStorage.Entry sessionEntry = mRecoverySessionStorage.get(uid, sessionId); @@ -702,13 +704,13 @@ public void setRecoverySecretTypes( * Destroys the session with the given {@code sessionId}. */ public void closeSession(@NonNull String sessionId) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); Objects.requireNonNull(sessionId, "invalid session"); mRecoverySessionStorage.remove(Binder.getCallingUid(), sessionId); } public void removeKey(@NonNull String alias) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); Objects.requireNonNull(alias, "alias is null"); int uid = Binder.getCallingUid(); int userId = UserHandle.getCallingUserId(); @@ -747,7 +749,7 @@ public String generateKey(@NonNull String alias) throws RemoteException { */ public String generateKeyWithMetadata(@NonNull String alias, @Nullable byte[] metadata) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); Objects.requireNonNull(alias, "alias is null"); int uid = Binder.getCallingUid(); int userId = UserHandle.getCallingUserId(); @@ -852,7 +854,7 @@ public String generateKeyWithMetadata(@NonNull String alias, @Nullable byte[] me * @return grant alias, which caller can use to access the key. */ public @Nullable String getKey(@NonNull String alias) throws RemoteException { - checkRecoverKeyStorePermission(); + checkRecoverKeyStoreGmsCorePermission(); Objects.requireNonNull(alias, "alias is null"); int uid = Binder.getCallingUid(); int userId = UserHandle.getCallingUserId(); @@ -998,6 +1000,27 @@ public void lockScreenSecretChanged(@NonNull LockscreenCredential credential, in } } + /** + * Removes recoverable keystore state for a recovery agent. + * + *

Run the cleanup on the key sync executor so it cannot race a previously scheduled key + * sync task. + */ + public void removeRecoverableKeystoreStateForRecoveryAgent(int userId, int uid) { + try { + mExecutorService.submit(() -> { + mRecoverySessionStorage.remove(uid); + mListenersStorage.remove(uid); + mCleanupManager.removeDataForRecoveryAgent(userId, uid); + }).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.e(TAG, "Interrupted while removing recoverable keystore state for uid " + uid, e); + } catch (ExecutionException e) { + Log.e(TAG, "Failed to remove recoverable keystore state for uid " + uid, e.getCause()); + } + } + /** * Starts a session to verify lock screen credentials provided by a remote device. */ @@ -1154,9 +1177,25 @@ private int lockPatternUtilsToKeyguardType(int credentialsType) { } private void checkRecoverKeyStorePermission() { - mContext.enforceCallingOrSelfPermission( - Manifest.permission.RECOVER_KEYSTORE, - "Caller " + Binder.getCallingUid() + " doesn't have RecoverKeyStore permission."); + checkRecoverKeyStorePermission(/*allowGmsCorePermission=*/ false); + } + + private void checkRecoverKeyStoreGmsCorePermission() { + checkRecoverKeyStorePermission(/*allowGmsCorePermission=*/ true); + } + + private void checkRecoverKeyStorePermission(boolean allowGmsCorePermission) { + boolean permissionGranted = mContext.checkCallingOrSelfPermission( + Manifest.permission.RECOVER_KEYSTORE) == PackageManager.PERMISSION_GRANTED; + if (!permissionGranted && allowGmsCorePermission) { + permissionGranted = mContext.checkCallingOrSelfPermission( + Manifest.permission.RECOVER_KEYSTORE_GMSCORE) + == PackageManager.PERMISSION_GRANTED; + } + if (!permissionGranted) { + throw new SecurityException("Caller " + Binder.getCallingUid() + + " doesn't have RecoverKeyStore permission."); + } int userId = UserHandle.getCallingUserId(); int uid = Binder.getCallingUid(); mCleanupManager.registerRecoveryAgent(userId, uid); diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java index bd9f0fdbe63dd..d5bd5f00fb250 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java @@ -64,6 +64,14 @@ public synchronized boolean hasListener(int recoveryAgentUid) { return mAgentIntents.get(recoveryAgentUid) != null; } + /** + * Removes the listener and pending snapshot state for the recovery agent. + */ + public synchronized void remove(int recoveryAgentUid) { + mAgentIntents.remove(recoveryAgentUid); + mAgentsWithPendingSnapshots.remove(recoveryAgentUid); + } + /** * Notifies recovery agent that new snapshot is available. If a recovery agent has not yet * registered a {@link PendingIntent}, remembers that a snapshot is pending for it, so that diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java index ef56a1e268543..2dcfa35dfd37e 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java @@ -35,6 +35,9 @@ import java.security.KeyStore.SecretKeyEntry; import java.security.KeyStoreException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; import java.util.Locale; import javax.crypto.spec.SecretKeySpec; @@ -105,6 +108,39 @@ public void deleteEntry(int userId, int uid, String alias) { } } + /** + * Deletes all application keys belonging to the given recovery agent. + */ + public void deleteEntriesForRecoveryAgent(int userId, int uid) { + String aliasPrefix = getInternalAliasPrefix(userId, uid); + List aliasesToDelete = new ArrayList<>(); + try { + Enumeration aliases = mKeyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (alias.startsWith(aliasPrefix)) { + aliasesToDelete.add(alias); + } + } + } catch (KeyStoreException e) { + throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); + } + + KeyStoreException deletionException = null; + for (String alias : aliasesToDelete) { + try { + mKeyStore.deleteEntry(alias); + } catch (KeyStoreException e) { + Log.e(TAG, "Failed to delete recoverable application key " + alias, e); + deletionException = e; + } + } + if (deletionException != null) { + throw new ServiceSpecificException( + ERROR_SERVICE_INTERNAL_ERROR, deletionException.getMessage()); + } + } + /** * Returns the alias in locksettins service's KeyStore namespace used for given application key. * @@ -117,7 +153,11 @@ public void deleteEntry(int userId, int uid, String alias) { * @return The alias. */ private String getInternalAlias(int userId, int uid, String alias) { - return APPLICATION_KEY_ALIAS_PREFIX + userId + "/" + uid + "/" + alias; + return getInternalAliasPrefix(userId, uid) + alias; + } + + private String getInternalAliasPrefix(int userId, int uid) { + return APPLICATION_KEY_ALIAS_PREFIX + userId + "/" + uid + "/"; } private String makeKeystoreEngineGrantString(int uid, String alias) { diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java index 7d5fd652ef38f..f1b789eccce8b 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java @@ -154,6 +154,16 @@ private void removeDataForUser(int userId) { mDatabase.removeUserFromAllTables(userId); } + /** + * Removes all data belonging to the given recovery agent. + */ + public synchronized void removeDataForRecoveryAgent(int userId, int uid) { + Log.d(TAG, "Removing data for recovery agent " + uid + " for user " + userId + "."); + mSnapshotStorage.remove(uid); + removeAllKeysForRecoveryAgentByPrefix(userId, uid); + mDatabase.removeRecoveryAgentFromAllTables(userId, uid); + } + /** * Removes keys from Android KeyStore for the recovery agent; * Doesn't remove encrypted key material from the database. @@ -171,4 +181,13 @@ private void removeAllKeysForRecoveryAgent(int userId, int uid) { } } } + + private void removeAllKeysForRecoveryAgentByPrefix(int userId, int uid) { + try { + mApplicationKeyStorage.deleteEntriesForRecoveryAgent(userId, uid); + } catch (ServiceSpecificException e) { + // Ignore errors during key removal. + Log.e(TAG, "Error while removing recoverable keys for uid " + uid, e); + } + } } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java index f2c55544e859c..32828c54ecbab 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java @@ -1346,6 +1346,38 @@ public void removeUserFromAllTables(int userId) { removeUserFromRootOfTrustTable(userId); } + /** + * Removes all entries for the given recovery agent without removing user-wide metadata. + */ + public void removeRecoveryAgentFromAllTables(int userId, int uid) { + SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase(); + String[] selectionArgs = { + Integer.toString(userId), + Integer.toString(uid) + }; + + db.beginTransaction(); + try { + // Use each table's own contract so ownership-column changes are caught during rebases. + deleteRecoveryAgentRows(db, KeysEntry.TABLE_NAME, KeysEntry.COLUMN_NAME_USER_ID, + KeysEntry.COLUMN_NAME_UID, selectionArgs); + deleteRecoveryAgentRows(db, RecoveryServiceMetadataEntry.TABLE_NAME, + RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID, + RecoveryServiceMetadataEntry.COLUMN_NAME_UID, selectionArgs); + deleteRecoveryAgentRows(db, RootOfTrustEntry.TABLE_NAME, + RootOfTrustEntry.COLUMN_NAME_USER_ID, RootOfTrustEntry.COLUMN_NAME_UID, + selectionArgs); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private static void deleteRecoveryAgentRows(SQLiteDatabase db, String tableName, + String userIdColumn, String uidColumn, String[] selectionArgs) { + db.delete(tableName, userIdColumn + " = ? AND " + uidColumn + " = ?", selectionArgs); + } + /** * Removes all entries for given userId from Keys table. * diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java index 599ba89478a25..60e363543bca0 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java @@ -21,7 +21,9 @@ /** * Contract for recoverable key database. Describes the tables present. * - * Make sure that {@code removeUserFromAllKnownTables} is updated, when new table is added. + * Make sure that {@link RecoverableKeyStoreDb#removeUserFromAllTables(int)} is updated when a table + * is added. If the table contains recovery-agent state, also update + * {@link RecoverableKeyStoreDb#removeRecoveryAgentFromAllTables(int, int)}. */ class RecoverableKeyStoreDbContract { /** diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java index ef6dfe96f58a6..b2a2ac0e8ebd0 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java @@ -80,7 +80,13 @@ public void remove(int uid, String sessionId) { if (mSessionsByUid.get(uid) == null) { return; } - mSessionsByUid.get(uid).removeIf(session -> session.mSessionId.equals(sessionId)); + mSessionsByUid.get(uid).removeIf(session -> { + if (!session.mSessionId.equals(sessionId)) { + return false; + } + session.destroy(); + return true; + }); } /** diff --git a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java index 4a8c2b52a80c2..013e02c963363 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java @@ -7,8 +7,9 @@ import android.app.ActivityManagerInternal; import android.content.pm.GosPackageState; import android.content.pm.GosPackageStateFlag; -import android.ext.KnownSystemPackages; import android.ext.DerivedPackageFlag; +import android.ext.KnownSystemPackages; +import android.ext.PackageId; import android.os.Binder; import android.os.Process; import android.os.RemoteException; @@ -19,7 +20,9 @@ import com.android.internal.pm.parsing.pkg.AndroidPackageInternal; import com.android.internal.pm.pkg.component.ParsedUsesPermission; import com.android.server.LocalServices; +import com.android.server.locksettings.LockSettingsInternal; import com.android.server.pm.PackageManagerLocal.GosPackageStateChangeCallback; +import com.android.server.pm.ext.PackageExt; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.PackageUserStateInternal; @@ -352,6 +355,38 @@ public static void onClearApplicationUserData(PackageManagerService pm, String p } } + static void onApplicationDataCleared( + PackageManagerService pm, String packageName, int userId) { + PackageStateInternal packageState = + pm.snapshotComputer().getPackageStateInternal(packageName); + if (shouldClearGmsCoreRecoverableKeystoreState(packageState)) { + clearGmsCoreRecoverableKeystoreState( + pm, userId, packageState.getAppId()); + } + } + + static boolean shouldClearGmsCoreRecoverableKeystoreState( + @Nullable PackageStateInternal packageState) { + return packageState != null + && !packageState.isSystem() + && PackageExt.get(packageState).getPackageId() == PackageId.GMS_CORE; + } + + static void clearGmsCoreRecoverableKeystoreState( + PackageManagerService pm, int userId, int appId) { + LockSettingsInternal lockSettings = + LocalServices.getService(LockSettingsInternal.class); + if (lockSettings == null) { + Slog.w(TAG, "LockSettingsInternal is unavailable"); + return; + } + + for (int realUserId : pm.resolveUserIds(userId)) { + lockSettings.removeRecoverableKeystoreStateForRecoveryAgent( + realUserId, UserHandle.getUid(realUserId, appId)); + } + } + private static void clearContactScopesStorage(PackageManagerService pm, int userId) { for (PackageStateInternal ps : pm.snapshotComputer().getPackageStates().values()) { PackageUserStateInternal us = ps.getUserStateOrDefault(userId); diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 201d99aa9e9af..955d9d1aad790 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -5176,6 +5176,10 @@ public void run() { callingUid); } } + if (succeeded) { + GosPackageStatePmHooks.onApplicationDataCleared( + PackageManagerService.this, packageName, userId); + } } if (succeeded) { // invoke DeviceStorageMonitor's update method to clear any notifications diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 7099c2e0bd468..3fb5e14a68077 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -353,14 +353,21 @@ public void clearPackageStateForUserLIF(PackageSetting ps, @CanBeALL @UserIdInt if (ps.getAppId() == SYSTEM_UID) { return; } + final boolean clearGmsCoreRecoverableKeystoreState = + GosPackageStatePmHooks.shouldClearGmsCoreRecoverableKeystoreState(ps); + final int appId = ps.getAppId(); mPm.mInjector.getBackgroundHandler().post(() -> { try { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, - "clearKeystoreData:" + ps.getAppId() + " for user: " + userId); - mAppDataHelper.clearKeystoreData(userId, ps.getAppId()); + "clearKeystoreData:" + appId + " for user: " + userId); + mAppDataHelper.clearKeystoreData(userId, appId); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } + if (clearGmsCoreRecoverableKeystoreState) { + GosPackageStatePmHooks.clearGmsCoreRecoverableKeystoreState( + mPm, userId, appId); + } }); } diff --git a/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java b/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java index 5abcf28bc28a5..4aa0d5fc4835b 100644 --- a/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java +++ b/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java @@ -41,6 +41,9 @@ public int overridePermissionState(String permission, int userId) { case Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER: flag = GmsCorePackageFlag.GRANT_PERMS_FOR_ICC_AUTHENTICATION; break; + case Manifest.permission.RECOVER_KEYSTORE_GMSCORE: + flag = GmsCorePackageFlag.GRANT_PERMS_FOR_RECOVER_KEYSTORE_GMSCORE; + break; default: return NO_PERMISSION_OVERRIDE; } @@ -99,7 +102,8 @@ public List addUsesPermissions() { var l = createUsesPerms( Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Manifest.permission.READ_PHONE_NUMBERS, - Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER + Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER, + Manifest.permission.RECOVER_KEYSTORE_GMSCORE ); res.addAll(l); return res; diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java index 0965916588920..bed579fd65c86 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java @@ -95,6 +95,7 @@ import java.util.Arrays; import java.util.Map; import java.util.Random; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import javax.crypto.KeyGenerator; @@ -172,6 +173,7 @@ public class RecoverableKeyStoreManagerTest { @Mock private ApplicationKeyStorage mApplicationKeyStorage; @Mock private CleanupManager mCleanupManager; @Mock private ScheduledExecutorService mExecutorService; + @Mock private Future mFuture; @Mock private LockSettingsService mLockSettingsService; @Spy private TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper; @@ -764,9 +766,8 @@ public void startRecoverySession_checksPermissionFirst() throws Exception { TEST_VAULT_CHALLENGE, ImmutableList.of(TEST_PROTECTION_PARAMS)); - verify(mMockContext, times(1)) - .enforceCallingOrSelfPermission( - eq(Manifest.permission.RECOVER_KEYSTORE), any()); + verify(mMockContext, times(1)).checkCallingOrSelfPermission( + eq(Manifest.permission.RECOVER_KEYSTORE)); } @Ignore("Causing breakages so ignoring to resolve, b/281583079") @@ -1133,6 +1134,42 @@ public void setSnapshotCreatedPendingIntent() throws Exception { verify(mMockListenersStorage).setSnapshotListener(eq(uid), any(PendingIntent.class)); } + @Test + public void removeRecoverableKeystoreStateForRecoveryAgent_removesOnlyTargetAgentState() + throws Exception { + int uid = 1234; + int otherUid = 5678; + mRecoverySessionStorage.add( + uid, + new RecoverySessionStorage.Entry( + TEST_SESSION_ID, + TEST_SECRET.clone(), + TEST_SALT.clone(), + TEST_VAULT_PARAMS.clone())); + mRecoverySessionStorage.add( + otherUid, + new RecoverySessionStorage.Entry( + TEST_SESSION_ID, + TEST_SECRET.clone(), + TEST_SALT.clone(), + TEST_VAULT_PARAMS.clone())); + when(mExecutorService.submit(any(Runnable.class))) + .thenAnswer( + invocation -> { + invocation.getArgument(0).run(); + return mFuture; + }); + + mRecoverableKeyStoreManager.removeRecoverableKeystoreStateForRecoveryAgent( + TEST_USER_ID, uid); + + verify(mFuture).get(); + assertThat(mRecoverySessionStorage.get(uid, TEST_SESSION_ID)).isNull(); + assertThat(mRecoverySessionStorage.get(otherUid, TEST_SESSION_ID)).isNotNull(); + verify(mMockListenersStorage).remove(uid); + verify(mCleanupManager).removeDataForRecoveryAgent(TEST_USER_ID, uid); + } + @Test public void setServerParams_updatesServerParams() throws Exception { int uid = Binder.getCallingUid(); diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java index 418d474523306..074a311d9a97d 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java @@ -49,6 +49,58 @@ public void hasListener_isTrueForRegisteredUid() { assertTrue(mStorage.hasListener(recoveryAgentUid)); } + @Test + public void remove_clearsListener() { + int recoveryAgentUid = 1000; + PendingIntent intent = + PendingIntent.getBroadcast( + InstrumentationRegistry.getTargetContext(), + /* requestCode= */ 1, + new Intent() + .setPackage( + InstrumentationRegistry.getTargetContext() + .getPackageName()), + /* flags= */ PendingIntent.FLAG_MUTABLE); + mStorage.setSnapshotListener(recoveryAgentUid, intent); + + mStorage.remove(recoveryAgentUid); + + assertFalse(mStorage.hasListener(recoveryAgentUid)); + } + + @Test + public void remove_clearsPendingSnapshot() throws Exception { + Context context = InstrumentationRegistry.getTargetContext(); + int recoveryAgentUid = 1000; + mStorage.recoverySnapshotAvailable(recoveryAgentUid); + mStorage.remove(recoveryAgentUid); + PendingIntent intent = + PendingIntent.getBroadcast( + context, + /* requestCode= */ 0, + new Intent(TEST_INTENT_ACTION).setPackage(context.getPackageName()), + /* flags= */ PendingIntent.FLAG_MUTABLE); + CountDownLatch latch = new CountDownLatch(1); + BroadcastReceiver broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + latch.countDown(); + } + }; + context.registerReceiver( + broadcastReceiver, + new IntentFilter(TEST_INTENT_ACTION), + Context.RECEIVER_EXPORTED_UNAUDITED); + + try { + mStorage.setSnapshotListener(recoveryAgentUid, intent); + assertFalse(latch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS)); + } finally { + context.unregisterReceiver(broadcastReceiver); + } + } + @Test public void setSnapshotListener_invokesIntentImmediatelyIfPreviouslyNotified() throws Exception { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorageTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorageTest.java new file mode 100644 index 0000000000000..6ff7d13d0c9e0 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorageTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2026 GrapheneOS + * + * 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.server.locksettings.recoverablekeystore.storage; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertThrows; + +import android.os.ServiceSpecificException; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.locksettings.recoverablekeystore.KeyStoreProxy; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.security.KeyStoreException; +import java.util.Arrays; +import java.util.Collections; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ApplicationKeyStorageTest { + private static final int USER_ID = 10; + private static final int UID = 1012345; + private static final String ALIAS_PREFIX = + "com.android.server.locksettings.recoverablekeystore/application/10/1012345/"; + private static final String MATCHING_ALIAS_1 = ALIAS_PREFIX + "first"; + private static final String MATCHING_ALIAS_2 = ALIAS_PREFIX + "second"; + + @Mock private KeyStoreProxy mKeyStore; + + private ApplicationKeyStorage mStorage; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mStorage = new ApplicationKeyStorage(mKeyStore); + } + + @Test + public void deleteEntriesForRecoveryAgent_deletesOnlyMatchingAliases() throws Exception { + when(mKeyStore.aliases()) + .thenReturn( + Collections.enumeration( + Arrays.asList( + MATCHING_ALIAS_1, + "com.android.server.locksettings.recoverablekeystore/application/11/1012345/first", + "com.android.server.locksettings.recoverablekeystore/application/10/1012346/first", + "com.android.server.locksettings.recoverablekeystore/application/10/10123450/first", + MATCHING_ALIAS_2))); + + mStorage.deleteEntriesForRecoveryAgent(USER_ID, UID); + + verify(mKeyStore).aliases(); + verify(mKeyStore).deleteEntry(MATCHING_ALIAS_1); + verify(mKeyStore).deleteEntry(MATCHING_ALIAS_2); + verifyNoMoreInteractions(mKeyStore); + } + + @Test + public void deleteEntriesForRecoveryAgent_continuesAfterDeletionFailure() throws Exception { + when(mKeyStore.aliases()) + .thenReturn( + Collections.enumeration(Arrays.asList(MATCHING_ALIAS_1, MATCHING_ALIAS_2))); + doThrow(new KeyStoreException("failure")).when(mKeyStore).deleteEntry(MATCHING_ALIAS_1); + + assertThrows( + ServiceSpecificException.class, + () -> mStorage.deleteEntriesForRecoveryAgent(USER_ID, UID)); + + verify(mKeyStore).deleteEntry(MATCHING_ALIAS_1); + verify(mKeyStore).deleteEntry(MATCHING_ALIAS_2); + } +} diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java index 1c9c6dc8d0db3..8065a2f6f26d2 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java @@ -16,7 +16,9 @@ package com.android.server.locksettings.recoverablekeystore.storage; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -27,6 +29,8 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.locksettings.recoverablekeystore.WrappedKey; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -108,5 +112,33 @@ public void verifyKnownUsers_newSerialNumber_deletesData() throws Exception { verify(mDatabase).setUserSerialNumber(USER_ID, USER_SERIAL_NUMBER_2); verify(mRecoverySnapshotStorage).remove(UID); } -} + @Test + public void verifyKnownUsers_removedUser_removesAllUserState() throws Exception { + Map knownSerialNumbers = new HashMap<>(); + knownSerialNumbers.put(USER_ID, USER_SERIAL_NUMBER); + when(mDatabase.getUserSerialNumbers()).thenReturn(knownSerialNumbers); + when(mDatabase.getRecoveryAgents(USER_ID)).thenReturn(List.of(UID)); + when(mDatabase.getPlatformKeyGenerationId(USER_ID)).thenReturn(1); + Map keys = new HashMap<>(); + keys.put("alias", new WrappedKey(new byte[] {1}, new byte[] {2}, null, 1)); + when(mDatabase.getAllKeys(USER_ID, UID, 1)).thenReturn(keys); + when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID)))).thenReturn(-1L); + + mManager.verifyKnownUsers(); + + verify(mRecoverySnapshotStorage).remove(UID); + verify(mApplicationKeyStorage).deleteEntry(USER_ID, UID, "alias"); + verify(mDatabase).removeUserFromAllTables(USER_ID); + } + + @Test + public void removeDataForRecoveryAgent_removesOnlyAgentState() { + mManager.removeDataForRecoveryAgent(USER_ID, UID); + + verify(mRecoverySnapshotStorage).remove(UID); + verify(mApplicationKeyStorage).deleteEntriesForRecoveryAgent(USER_ID, UID); + verify(mDatabase).removeRecoveryAgentFromAllTables(USER_ID, UID); + verify(mDatabase, never()).removeUserFromAllTables(anyInt()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java index 036742559ef69..b96136e6f0c6d 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java @@ -448,6 +448,52 @@ public void removeUserFromAllTables_removesData() throws Exception { assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEmpty(); } + @Test + public void removeRecoveryAgentFromAllTables_removesOnlyAgentData() throws Exception { + int userId = 12; + int uid = 120001; + int otherUid = 120002; + int generationId = 24; + long userSerialNumber = 42L; + String alias = "target"; + String otherAlias = "other"; + WrappedKey wrappedKey = + new WrappedKey( + getUtf8Bytes("nonce"), + getUtf8Bytes("key"), + /* metadata= */ null, + generationId); + mRecoverableKeyStoreDb.setPlatformKeyGenerationId(userId, generationId); + mRecoverableKeyStoreDb.setUserSerialNumber(userId, userSerialNumber); + mRecoverableKeyStoreDb.insertKey(userId, uid, alias, wrappedKey); + mRecoverableKeyStoreDb.insertKey(userId, otherUid, otherAlias, wrappedKey); + mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, new int[] {1}); + mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, otherUid, new int[] {2}); + mRecoverableKeyStoreDb.setRecoveryServiceCertSerial(userId, uid, TEST_ROOT_CERT_ALIAS, 1L); + mRecoverableKeyStoreDb.setRecoveryServiceCertSerial( + userId, otherUid, TEST_ROOT_CERT_ALIAS, 2L); + + mRecoverableKeyStoreDb.removeRecoveryAgentFromAllTables(userId, uid); + + assertThat(mRecoverableKeyStoreDb.getKey(uid, alias)).isNull(); + assertThat(mRecoverableKeyStoreDb.getKey(otherUid, otherAlias)).isNotNull(); + assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEmpty(); + assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, otherUid)) + .isEqualTo(new int[] {2}); + assertThat( + mRecoverableKeyStoreDb.getRecoveryServiceCertSerial( + userId, uid, TEST_ROOT_CERT_ALIAS)) + .isNull(); + assertThat( + mRecoverableKeyStoreDb.getRecoveryServiceCertSerial( + userId, otherUid, TEST_ROOT_CERT_ALIAS)) + .isEqualTo(2L); + assertThat(mRecoverableKeyStoreDb.getPlatformKeyGenerationId(userId)) + .isEqualTo(generationId); + assertThat(mRecoverableKeyStoreDb.getUserSerialNumbers().get(userId)) + .isEqualTo(userSerialNumber); + } + @Test public void setRecoveryStatus_withSingleKey() { int userId = 12;