From bc03ebd199ae6a7c3bcde6d5eb56db5aebd3e8c8 Mon Sep 17 00:00:00 2001 From: Dome Pongmongkol Date: Tue, 14 Apr 2026 12:08:29 -0700 Subject: [PATCH 1/9] wip --- .../AndroidPlatformComponentsFactory.java | 24 ++++ ...ndroidAuthSdkStorageEncryptionManager.java | 22 +++- .../DeviceRegistrationClientApplication.kt | 2 +- ...idAuthSdkStorageEncryptionManagerTest.java | 92 ++++++++++++- .../identity/common/crypto/MockData.java | 4 +- .../BrokerDiscoveryClientTests.kt | 122 ++++++++++++++++++ 6 files changed, 254 insertions(+), 12 deletions(-) diff --git a/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java b/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java index 069323cf66..786e869c90 100644 --- a/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java +++ b/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java @@ -37,6 +37,7 @@ import com.microsoft.identity.common.internal.ui.AndroidAuthorizationStrategyFactory; import com.microsoft.identity.common.internal.ui.browser.AndroidBrowserSelector; import com.microsoft.identity.common.internal.util.WorkProfileUtil; +import com.microsoft.identity.common.java.AuthenticationSettings; import com.microsoft.identity.common.java.WarningType; import com.microsoft.identity.common.java.flighting.CommonFlight; import com.microsoft.identity.common.java.flighting.CommonFlightsManager; @@ -121,6 +122,29 @@ private static IPlatformComponents create(@NonNull final Context context, return builder.build(); } + /** + * Creates an {@link IPlatformComponents} object from a {@link Context}, + * with the storage encryption manager configured to always use the Android KeyStore + * for encryption, ignoring any predefined key set via {@link AuthenticationSettings}. + *

+ * This is intended for components (e.g. Device Registration API, Broker API) + * that should not depend on the MSAL/ADAL predefined key lifecycle. + * + * @param context an application context. + **/ + public static IPlatformComponents createFromContextWithKeystoreOnlyEncryption( + @NonNull final Context context) { + initializeGlobalStates(context); + + final PlatformComponents.PlatformComponentsBuilder builder = PlatformComponents.builder(); + fillBuilderWithBasicImplementations(builder, context, null, null); + + // Override the storage supplier with a keystore-only encryption manager. + builder.storageSupplier(new AndroidStorageSupplier(context, + new AndroidAuthSdkStorageEncryptionManager(context, true))); + return builder.build(); + } + /** * Fill {@link PlatformComponents.PlatformComponentsBuilder} * with Android implementations that could be shared with other Factories, i.e. Broker. diff --git a/common/src/main/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManager.java b/common/src/main/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManager.java index 283cdecd57..b8e317c659 100644 --- a/common/src/main/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManager.java +++ b/common/src/main/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManager.java @@ -28,6 +28,8 @@ import com.microsoft.identity.common.java.crypto.StorageEncryptionManager; import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider; import com.microsoft.identity.common.java.crypto.key.PredefinedKeyProvider; +import com.microsoft.identity.common.java.exception.ClientException; +import com.microsoft.identity.common.java.exception.ErrorStrings; import com.microsoft.identity.common.logging.Logger; import java.util.Collections; @@ -57,7 +59,21 @@ public class AndroidAuthSdkStorageEncryptionManager extends StorageEncryptionMan private final ISecretKeyProvider mKeyStoreKeyProvider; public AndroidAuthSdkStorageEncryptionManager(@NonNull final Context context) { - if (AuthenticationSettings.INSTANCE.getSecretKeyData() == null) { + this(context, false); + } + + /** + * @param context an application context. + * @param useKeystoreOnly if true, always use the KeyStore-backed key for encryption, + * ignoring any predefined key set via {@link AuthenticationSettings}. + * For decryption of data encrypted with a predefined key, the manager + * will attempt to use the predefined key if available (migration), + * or gracefully fall back to the keystore key (which will fail with + * a {@link com.microsoft.identity.common.java.exception.ClientException}. + */ + public AndroidAuthSdkStorageEncryptionManager(@NonNull final Context context, + final boolean useKeystoreOnly) { + if (useKeystoreOnly || AuthenticationSettings.INSTANCE.getSecretKeyData() == null) { mPredefinedKeyProvider = null; } else { mPredefinedKeyProvider = new PredefinedKeyProvider("USER_DEFINED_KEY", @@ -83,7 +99,7 @@ public ISecretKeyProvider getKeyProviderForEncryption() { @Override @NonNull - public List getKeyProviderForDecryption(byte[] cipherText) { + public List getKeyProviderForDecryption(byte[] cipherText) throws ClientException { final String methodTag = TAG + ":getKeyLoaderForDecryption"; final String keyIdentifier = getKeyIdentifierFromCipherText(cipherText); @@ -91,7 +107,7 @@ public List getKeyProviderForDecryption(byte[] cipherText) { if (mPredefinedKeyProvider != null) { return Collections.singletonList(mPredefinedKeyProvider); } else { - throw new IllegalStateException( + throw new ClientException(ErrorStrings.DECRYPTION_FAILED, "Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, " + "but mPredefinedKeyProvider is null."); } diff --git a/common/src/main/java/com/microsoft/identity/deviceregistration/api/DeviceRegistrationClientApplication.kt b/common/src/main/java/com/microsoft/identity/deviceregistration/api/DeviceRegistrationClientApplication.kt index 724060ab4b..36d081b2c5 100644 --- a/common/src/main/java/com/microsoft/identity/deviceregistration/api/DeviceRegistrationClientApplication.kt +++ b/common/src/main/java/com/microsoft/identity/deviceregistration/api/DeviceRegistrationClientApplication.kt @@ -85,7 +85,7 @@ class DeviceRegistrationClientApplication { */ @Throws(ClientException::class) constructor(context: Context) { - val components = AndroidPlatformComponentsFactory.createFromContext(context) + val components = AndroidPlatformComponentsFactory.createFromContextWithKeystoreOnlyEncryption(context) mController = buildController( context, components, diff --git a/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java b/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java index ab7399ef61..9e82b67fa4 100644 --- a/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java +++ b/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java @@ -35,6 +35,7 @@ import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider; import com.microsoft.identity.common.java.crypto.key.KeyUtil; import com.microsoft.identity.common.java.crypto.key.PredefinedKeyProvider; +import com.microsoft.identity.common.java.exception.ClientException; import org.junit.Assert; import org.junit.Before; @@ -82,7 +83,7 @@ public void testGetEncryptionKey_PreDefinedKeyProvided() { * try getting a decryption key when a predefined key is NOT provided. */ @Test - public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey() { + public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey() throws ClientException { final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); final List keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY); @@ -96,7 +97,7 @@ public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey() { * try getting a decryption key when a predefined key is provided. */ @Test - public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey_PreDefinedKeyProvided() { + public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey_PreDefinedKeyProvided() throws ClientException { AuthenticationSettings.INSTANCE.setSecretKey(PREDEFINED_KEY); final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); final List keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY); @@ -111,18 +112,19 @@ public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey_PreDefinedKeyPr * try getting a decryption key when a predefined key is NOT provided. */ @Test - public void testGetDecryptionKey_ForDataEncryptedWithPreDefinedKey() { + public void testGetDecryptionKey_ForDataEncryptedWithPreDefinedKey() throws ClientException { final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); try { final List keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); - } catch (IllegalStateException ex) { + Assert.fail("Expected ClientException"); + } catch (ClientException ex) { Assert.assertEquals( "Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, but mPredefinedKeyProvider is null.", ex.getMessage()); } } - public void testGetDecryptionKey_ForUnencryptedText_returns_empty_keyprovider() { + public void testGetDecryptionKey_ForUnencryptedText_returns_empty_keyprovider() throws ClientException { AuthenticationSettings.INSTANCE.setIgnoreKeyProviderNotFoundError(false); final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); final List keyproviderList = manager.getKeyProviderForDecryption("Unencrypted".getBytes(ENCODING_UTF8)); @@ -134,7 +136,7 @@ public void testGetDecryptionKey_ForUnencryptedText_returns_empty_keyprovider() * try getting a decryption key when a predefined key is provided. */ @Test - public void testGetDecryptionKey_ForDataEncryptedWithPreDefinedKey_PreDefinedKeyProvided() { + public void testGetDecryptionKey_ForDataEncryptedWithPreDefinedKey_PreDefinedKeyProvided() throws ClientException { AuthenticationSettings.INSTANCE.setSecretKey(PREDEFINED_KEY); final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); final List keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); @@ -143,4 +145,82 @@ public void testGetDecryptionKey_ForDataEncryptedWithPreDefinedKey_PreDefinedKey Assert.assertTrue(keyproviderList.get(0) instanceof PredefinedKeyProvider); Assert.assertEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(keyproviderList.get(0))); } + + // ==================== useKeystoreOnly=true tests ==================== + + /** + * In useKeystoreOnly mode, getKeyProviderForEncryption() should always return the + * KeyStore-backed key, even when a predefined key is set in AuthenticationSettings. + */ + @Test + public void testKeystoreOnly_GetEncryptionKey_IgnoresPredefinedKey() { + AuthenticationSettings.INSTANCE.setSecretKey(PREDEFINED_KEY); + final AndroidAuthSdkStorageEncryptionManager manager = + new AndroidAuthSdkStorageEncryptionManager(context, true); + + final ISecretKeyProvider provider = manager.getKeyProviderForEncryption(); + Assert.assertTrue(provider instanceof KeyStoreBackedSecretKeyProvider); + Assert.assertNotEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(provider)); + } + + /** + * In useKeystoreOnly mode, getKeyProviderForEncryption() should return the KeyStore-backed key + * when no predefined key is set (same as default mode). + */ + @Test + public void testKeystoreOnly_GetEncryptionKey_NoPredefinedKey() { + final AndroidAuthSdkStorageEncryptionManager manager = + new AndroidAuthSdkStorageEncryptionManager(context, true); + + final ISecretKeyProvider provider = manager.getKeyProviderForEncryption(); + Assert.assertTrue(provider instanceof KeyStoreBackedSecretKeyProvider); + } + + /** + * In useKeystoreOnly mode, when encountering data encrypted with a predefined key + * and the predefined key is NOT available, should throw ClientException + * (not IllegalStateException), so EncryptedNameValueStorage.get() can catch it. + */ + @Test + public void testKeystoreOnly_GetDecryptionKey_PredefinedKeyData_NotAvailable() throws ClientException { + final AndroidAuthSdkStorageEncryptionManager manager = + new AndroidAuthSdkStorageEncryptionManager(context, true); + try { + manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); + Assert.fail("Expected ClientException"); + } catch (ClientException ex) { + Assert.assertTrue(ex.getMessage().contains("mPredefinedKeyProvider is null")); + } + } + + /** + * In useKeystoreOnly mode, even when a predefined key IS set in AuthenticationSettings,\n * encountering U001-encrypted data should still throw ClientException + * because mPredefinedKeyProvider is always null in this mode. + */ + @Test + public void testKeystoreOnly_GetDecryptionKey_PredefinedKeyData_Available_StillThrows() throws ClientException { + AuthenticationSettings.INSTANCE.setSecretKey(PREDEFINED_KEY); + final AndroidAuthSdkStorageEncryptionManager manager = + new AndroidAuthSdkStorageEncryptionManager(context, true); + try { + manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); + Assert.fail("Expected ClientException"); + } catch (ClientException ex) { + Assert.assertTrue(ex.getMessage().contains("mPredefinedKeyProvider is null")); + } + } + + /** + * In useKeystoreOnly mode, data encrypted with the keystore key should decrypt normally. + */ + @Test + public void testKeystoreOnly_GetDecryptionKey_ForDataEncryptedWithKeyStoreKey() throws ClientException { + final AndroidAuthSdkStorageEncryptionManager manager = + new AndroidAuthSdkStorageEncryptionManager(context, true); + final List keyproviderList = + manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY); + + Assert.assertEquals(1, keyproviderList.size()); + Assert.assertTrue(keyproviderList.get(0) instanceof KeyStoreBackedSecretKeyProvider); + } } diff --git a/common/src/test/java/com/microsoft/identity/common/crypto/MockData.java b/common/src/test/java/com/microsoft/identity/common/crypto/MockData.java index ab6a661d0e..cb99b72395 100644 --- a/common/src/test/java/com/microsoft/identity/common/crypto/MockData.java +++ b/common/src/test/java/com/microsoft/identity/common/crypto/MockData.java @@ -29,11 +29,11 @@ private MockData(){} // Value extracted from the legacy StorageHelper. // Data Set 1 - Predefined key. - static final byte[] PREDEFINED_KEY = new byte[]{22, 78, -69, -66, 84, -65, 119, -9, -34, -80, 60, 67, -12, -117, 86, -47, -84, -24, -18, 121, 70, 32, -110, 51, -93, -10, -93, -110, 124, -68, -42, -119}; + public static final byte[] PREDEFINED_KEY = new byte[]{22, 78, -69, -66, 84, -65, 119, -9, -34, -80, 60, 67, -12, -117, 86, -47, -84, -24, -18, 121, 70, 32, -110, 51, -93, -10, -93, -110, 124, -68, -42, -119}; static final byte[] TEXT_ENCRYPTED_BY_PREDEFINED_KEY = "cE1VTAwMeHz7BCCH/27kWvMYYMsGamVenQk6w+YJ14JnFBi6fJ1D8FrdLe8ZSX/FeU1apYKsj9d1fNoMD4kR62XfPMytA3P2XpXEQtkblP6F6A5R74F".getBytes(ENCODING_UTF8); // Data Set 2 - Another predefined key. - static final byte[] ANOTHER_PREDEFINED_KEY = new byte[]{122, 75, 49, 112, 36, 126, 5, 35, 46, 45, -61, -61, 55, 105, 9, -123, 115, 27, 35, -54, -49, 14, -16, 49, -74, -88, -29, -15, -33, -13, 100, 118}; + public static final byte[] ANOTHER_PREDEFINED_KEY = new byte[]{122, 75, 49, 112, 36, 126, 5, 35, 46, 45, -61, -61, 55, 105, 9, -123, 115, 27, 35, -54, -49, 14, -16, 49, -74, -88, -29, -15, -33, -13, 100, 118}; // Data Set 3 - Android KeyStore-wrapped key. static final byte[] ANDROID_WRAPPED_KEY = new byte[]{122, 75, 49, 112, 36, 126, 5, 35, 46, 45, -61, -61, 55, 105, 9, -123, 115, 27, 35, -54, -49, 14, -16, 49, -74, -88, -29, -15, -33, -13, 100, 118}; diff --git a/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt b/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt index 4a38b3038c..798aef499f 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt +++ b/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt @@ -23,6 +23,11 @@ package com.microsoft.identity.common.internal.activebrokerdiscovery import android.os.Bundle +import androidx.test.core.app.ApplicationProvider +import com.microsoft.identity.common.adal.internal.AuthenticationSettings +import com.microsoft.identity.common.components.AndroidStorageSupplier +import com.microsoft.identity.common.crypto.AndroidAuthSdkStorageEncryptionManager +import com.microsoft.identity.common.crypto.MockData import com.microsoft.identity.common.exception.BrokerCommunicationException import com.microsoft.identity.common.internal.broker.BrokerData import com.microsoft.identity.common.internal.broker.BrokerData.Companion.prodCompanyPortal @@ -30,8 +35,15 @@ import com.microsoft.identity.common.internal.broker.BrokerData.Companion.prodMi import com.microsoft.identity.common.internal.broker.ipc.AbstractIpcStrategyWithServiceValidation import com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle import com.microsoft.identity.common.internal.broker.ipc.IIpcStrategy +import com.microsoft.identity.common.internal.cache.ClientActiveBrokerCache +import com.microsoft.identity.common.internal.cache.SharedPreferencesFileManager +import com.microsoft.identity.common.java.crypto.StorageEncryptionManager +import com.microsoft.identity.common.java.crypto.key.AES256SecretKeyGenerator +import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider +import com.microsoft.identity.common.java.crypto.key.PredefinedKeyProvider import com.microsoft.identity.common.java.exception.ClientException import com.microsoft.identity.common.java.exception.ClientException.ONLY_SUPPORTS_ACCOUNT_MANAGER_ERROR_CODE +import com.microsoft.identity.common.java.exception.ErrorStrings import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.Assert @@ -1003,4 +1015,114 @@ class BrokerDiscoveryClientTests { } ) } + + /** + * End-to-end test for the encryption key mismatch bug. + * + * Simulates Company Portal scenario: + * 1. MSAL sets a predefined key → cache data is encrypted with it (U001 prefix). + * 2. On next launch, WPJ API uses a different key for encryption (simulating keystore). + * 3. Reading cache fails decryption (ClientException caught by EncryptedNameValueStorage) → null. + * 4. BrokerDiscoveryClient falls back to IPC discovery — no crash. + * 5. After IPC, cache is re-populated with the new key and is readable. + * + * Uses real StorageEncryptionManager subclasses and ClientActiveBrokerCache. + * Uses a second PredefinedKeyProvider to stand in for the Android KeyStore key + * (which is unavailable in Robolectric). + */ + @Test + fun testCacheDecryptionFailure_FallsBackToIpcDiscovery() { + val context = ApplicationProvider.getApplicationContext() + + // A mock "keystore" key provider using a different key AND a different identifier than + // the predefined one. PredefinedKeyProvider always uses "U001", so we need a custom + // ISecretKeyProvider with a distinct identifier (e.g. "KS01") to simulate the real + // KeyStoreBackedSecretKeyProvider behavior. + val mockKeystoreKeyProvider = object : ISecretKeyProvider { + override val alias: String = "MOCK_KEYSTORE_KEY" + override val keyTypeIdentifier: String = "KS01" + override val key: javax.crypto.SecretKey = AES256SecretKeyGenerator.generateKeyFromRawBytes(MockData.ANOTHER_PREDEFINED_KEY) + override val cipherTransformation: String = "AES/CBC/PKCS5Padding" + } + + // Step 1: Write cache data using the predefined key (simulates MSAL-initialized path). + AuthenticationSettings.INSTANCE.setSecretKey(MockData.PREDEFINED_KEY) + val writeEncryptionManager = AndroidAuthSdkStorageEncryptionManager(context) + val writeSupplier = AndroidStorageSupplier(context, writeEncryptionManager) + val writeCache = ClientActiveBrokerCache.getBrokerSdkCache(writeSupplier) + writeCache.setCachedActiveBroker(prodMicrosoftAuthenticator) + + // Verify data was written successfully. + Assert.assertEquals(prodMicrosoftAuthenticator, writeCache.getCachedActiveBroker()) + + // Step 2: Clear the predefined key and SharedPreferencesFileManager cache + // (simulates app restart where MSAL hasn't initialized). + AuthenticationSettings.INSTANCE.clearSecretKeysForTestCases() + SharedPreferencesFileManager.clearSingletonCache() + + // Step 3: Create a "keystore-only" encryption manager that uses the mock keystore key + // and throws ClientException for U001-encrypted data (simulates the fix). + val readEncryptionManager = object : StorageEncryptionManager() { + override fun getKeyProviderForEncryption(): ISecretKeyProvider { + return mockKeystoreKeyProvider + } + + override fun getKeyProviderForDecryption(cipherText: ByteArray): List { + val keyIdentifier = getKeyIdentifierFromCipherText(cipherText) + if (PredefinedKeyProvider.USER_PROVIDED_KEY_IDENTIFIER.equals(keyIdentifier, ignoreCase = true)) { + throw ClientException( + ErrorStrings.DECRYPTION_FAILED, + "Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, " + + "but mPredefinedKeyProvider is null." + ) + } + // For data encrypted with the mock keystore key ("KS01"), return it. + return listOf(mockKeystoreKeyProvider) + } + } + val readSupplier = AndroidStorageSupplier(context, readEncryptionManager) + val readCache = ClientActiveBrokerCache.getBrokerSdkCache(readSupplier) + + // Step 4: Use this cache in BrokerDiscoveryClient — should fall back to IPC, not crash. + val client = BrokerDiscoveryClient( + brokerCandidates = setOf( + prodMicrosoftAuthenticator, prodCompanyPortal + ), + getActiveBrokerFromAccountManager = { + throw IllegalStateException("Should not fall back to AccountManager when IPC succeeds") + }, + ipcStrategy = object : IIpcStrategy { + override fun communicateToBroker(bundle: BrokerOperationBundle): Bundle { + val returnBundle = Bundle() + returnBundle.putString( + BrokerDiscoveryClient.ACTIVE_BROKER_PACKAGE_NAME_BUNDLE_KEY, + prodCompanyPortal.packageName + ) + returnBundle.putString( + BrokerDiscoveryClient.ACTIVE_BROKER_SIGNING_CERTIFICATE_THUMBPRINT_BUNDLE_KEY, + prodCompanyPortal.signingCertificateThumbprint + ) + return returnBundle + } + override fun isSupportedByTargetedBroker(targetedBrokerPackageName: String): Boolean { + return true + } + override fun getType(): IIpcStrategy.Type { + return IIpcStrategy.Type.CONTENT_PROVIDER + } + }, + cache = readCache, + isPackageInstalled = { + it == prodMicrosoftAuthenticator || it == prodCompanyPortal + }, + isValidBroker = { true } + ) + + // Should NOT crash — should fall back to IPC and discover Company Portal. + val result = client.getActiveBroker() + Assert.assertEquals(prodCompanyPortal, result) + + // After IPC re-populates the cache, it should be readable with the mock keystore key. + Assert.assertEquals(prodCompanyPortal, readCache.getCachedActiveBroker()) + } } \ No newline at end of file From 93e3b3a10745c0a57cedfe6b72d48d9829296c79 Mon Sep 17 00:00:00 2001 From: Dome Pongmongkol Date: Tue, 14 Apr 2026 13:36:45 -0700 Subject: [PATCH 2/9] copilot --- .../components/AndroidPlatformComponentsFactory.java | 4 ++-- .../AndroidAuthSdkStorageEncryptionManager.java | 12 +++++++----- .../AndroidAuthSdkStorageEncryptionManagerTest.java | 1 + .../BrokerDiscoveryClientTests.kt | 8 ++++++++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java b/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java index 786e869c90..04c6aef432 100644 --- a/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java +++ b/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java @@ -28,6 +28,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.microsoft.identity.common.adal.internal.AuthenticationSettings; import com.microsoft.identity.common.crypto.AndroidAuthSdkStorageEncryptionManager; import com.microsoft.identity.common.internal.net.cache.HttpCache; import com.microsoft.identity.common.internal.platform.AndroidBroadcaster; @@ -37,7 +38,6 @@ import com.microsoft.identity.common.internal.ui.AndroidAuthorizationStrategyFactory; import com.microsoft.identity.common.internal.ui.browser.AndroidBrowserSelector; import com.microsoft.identity.common.internal.util.WorkProfileUtil; -import com.microsoft.identity.common.java.AuthenticationSettings; import com.microsoft.identity.common.java.WarningType; import com.microsoft.identity.common.java.flighting.CommonFlight; import com.microsoft.identity.common.java.flighting.CommonFlightsManager; @@ -125,7 +125,7 @@ private static IPlatformComponents create(@NonNull final Context context, /** * Creates an {@link IPlatformComponents} object from a {@link Context}, * with the storage encryption manager configured to always use the Android KeyStore - * for encryption, ignoring any predefined key set via {@link AuthenticationSettings}. + * for encryption, ignoring any predefined key set via {@link AuthenticationSettings#setSecretKey(byte[])}. *

* This is intended for components (e.g. Device Registration API, Broker API) * that should not depend on the MSAL/ADAL predefined key lifecycle. diff --git a/common/src/main/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManager.java b/common/src/main/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManager.java index b8e317c659..f80ac8f17a 100644 --- a/common/src/main/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManager.java +++ b/common/src/main/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManager.java @@ -66,10 +66,12 @@ public AndroidAuthSdkStorageEncryptionManager(@NonNull final Context context) { * @param context an application context. * @param useKeystoreOnly if true, always use the KeyStore-backed key for encryption, * ignoring any predefined key set via {@link AuthenticationSettings}. - * For decryption of data encrypted with a predefined key, the manager - * will attempt to use the predefined key if available (migration), - * or gracefully fall back to the keystore key (which will fail with - * a {@link com.microsoft.identity.common.java.exception.ClientException}. + * The predefined key is never captured in this mode. + * Attempting to decrypt data that was encrypted with a predefined key + * (U001 identifier) will throw + * {@link com.microsoft.identity.common.java.exception.ClientException}, + * allowing callers (e.g. {@code EncryptedNameValueStorage}) to treat it + * as a cache miss and self-heal. */ public AndroidAuthSdkStorageEncryptionManager(@NonNull final Context context, final boolean useKeystoreOnly) { @@ -100,7 +102,7 @@ public ISecretKeyProvider getKeyProviderForEncryption() { @Override @NonNull public List getKeyProviderForDecryption(byte[] cipherText) throws ClientException { - final String methodTag = TAG + ":getKeyLoaderForDecryption"; + final String methodTag = TAG + ":getKeyProviderForDecryption"; final String keyIdentifier = getKeyIdentifierFromCipherText(cipherText); if (PredefinedKeyProvider.USER_PROVIDED_KEY_IDENTIFIER.equalsIgnoreCase(keyIdentifier)) { diff --git a/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java b/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java index 9e82b67fa4..e72446df03 100644 --- a/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java +++ b/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java @@ -124,6 +124,7 @@ public void testGetDecryptionKey_ForDataEncryptedWithPreDefinedKey() throws Clie } } + @Test public void testGetDecryptionKey_ForUnencryptedText_returns_empty_keyprovider() throws ClientException { AuthenticationSettings.INSTANCE.setIgnoreKeyProviderNotFoundError(false); final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); diff --git a/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt b/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt index 798aef499f..1f78fe0d3d 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt +++ b/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt @@ -22,6 +22,7 @@ // THE SOFTWARE. package com.microsoft.identity.common.internal.activebrokerdiscovery +import android.content.Context import android.os.Bundle import androidx.test.core.app.ApplicationProvider import com.microsoft.identity.common.adal.internal.AuthenticationSettings @@ -1034,6 +1035,13 @@ class BrokerDiscoveryClientTests { fun testCacheDecryptionFailure_FallsBackToIpcDiscovery() { val context = ApplicationProvider.getApplicationContext() + // Ensure test isolation: clear any leftover data in the broker SDK cache SharedPreferences + // and the SharedPreferencesFileManager singleton cache. + context.getSharedPreferences("BROKER_METADATA_CACHE_STORE_ON_BROKER_SDK_SIDE", Context.MODE_PRIVATE) + .edit().clear().commit() + SharedPreferencesFileManager.clearSingletonCache() + AuthenticationSettings.INSTANCE.clearSecretKeysForTestCases() + // A mock "keystore" key provider using a different key AND a different identifier than // the predefined one. PredefinedKeyProvider always uses "U001", so we need a custom // ISecretKeyProvider with a distinct identifier (e.g. "KS01") to simulate the real From c894a25957cef9750363da795428d81446279d1c Mon Sep 17 00:00:00 2001 From: Dome Pongmongkol Date: Tue, 14 Apr 2026 15:05:22 -0700 Subject: [PATCH 3/9] changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index b0fd8079f2..a904d416b0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [PATCH] Fix: WPJ's BrokerDiscovery crash due to shared encryption key with MSAL (#3081) - [PATCH] Optimize AcquireTokenSilent save path: replace keySet() decrypt-all with in-memory map lookup in removeAccount()/removeCredential(), add telemetry for deleteAccessTokensWithIntersectingScopes, and remove unused elapsed_time_save_account_shared_preferences attribute (#3074) - [MINOR] Add DeviceRegistrationClientApplication as public API for OneAuth device registration with mandatory correlationId, DeviceState and DrsDiscoveryEndpoint enums (#3073) - [MINOR] Move device registration protocol types, domain types, controller, and packer from broker to common to enable OneAuth device registration support (#3066) From 5f1adf65dfe8afbaade054d7d3ba8a11ac8df55f Mon Sep 17 00:00:00 2001 From: Dome Pongmongkol <19558668+rpdome@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:07:27 -0700 Subject: [PATCH 4/9] Update common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../crypto/AndroidAuthSdkStorageEncryptionManagerTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java b/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java index e72446df03..f431499203 100644 --- a/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java +++ b/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java @@ -195,7 +195,8 @@ public void testKeystoreOnly_GetDecryptionKey_PredefinedKeyData_NotAvailable() t } /** - * In useKeystoreOnly mode, even when a predefined key IS set in AuthenticationSettings,\n * encountering U001-encrypted data should still throw ClientException + * In useKeystoreOnly mode, even when a predefined key IS set in AuthenticationSettings, + * encountering U001-encrypted data should still throw ClientException * because mPredefinedKeyProvider is always null in this mode. */ @Test From 07b7576b0cae2f3bea93a51110ef52609fff0171 Mon Sep 17 00:00:00 2001 From: Dome Pongmongkol <19558668+rpdome@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:07:38 -0700 Subject: [PATCH 5/9] Update common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../common/components/AndroidPlatformComponentsFactory.java | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java b/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java index 04c6aef432..3d7a96a8ba 100644 --- a/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java +++ b/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java @@ -28,7 +28,6 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import com.microsoft.identity.common.adal.internal.AuthenticationSettings; import com.microsoft.identity.common.crypto.AndroidAuthSdkStorageEncryptionManager; import com.microsoft.identity.common.internal.net.cache.HttpCache; import com.microsoft.identity.common.internal.platform.AndroidBroadcaster; From 692e70a5b3bfb398fa41efb974322950c8068fd3 Mon Sep 17 00:00:00 2001 From: Dome Pongmongkol Date: Tue, 14 Apr 2026 15:16:49 -0700 Subject: [PATCH 6/9] changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index a904d416b0..88aca1e0f4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ vNext ---------- -- [PATCH] Fix: WPJ's BrokerDiscovery crash due to shared encryption key with MSAL (#3081) +- [PATCH] Fix: WPJ's BrokerDiscovery cache crash due to shared predefined encryption key with MSAL (#3081) - [PATCH] Optimize AcquireTokenSilent save path: replace keySet() decrypt-all with in-memory map lookup in removeAccount()/removeCredential(), add telemetry for deleteAccessTokensWithIntersectingScopes, and remove unused elapsed_time_save_account_shared_preferences attribute (#3074) - [MINOR] Add DeviceRegistrationClientApplication as public API for OneAuth device registration with mandatory correlationId, DeviceState and DrsDiscoveryEndpoint enums (#3073) - [MINOR] Move device registration protocol types, domain types, controller, and packer from broker to common to enable OneAuth device registration support (#3066) From bfb19c1dbcfd28f2a9b33d6ac0925db3e85297f3 Mon Sep 17 00:00:00 2001 From: Dome Pongmongkol Date: Tue, 14 Apr 2026 15:21:51 -0700 Subject: [PATCH 7/9] address copilot comments --- .../BrokerDiscoveryClientTests.kt | 188 ++++++++++-------- 1 file changed, 102 insertions(+), 86 deletions(-) diff --git a/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt b/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt index 1f78fe0d3d..aa60aca701 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt +++ b/common/src/test/java/com/microsoft/identity/common/internal/activebrokerdiscovery/BrokerDiscoveryClientTests.kt @@ -58,6 +58,14 @@ import java.util.concurrent.atomic.AtomicInteger @RunWith(RobolectricTestRunner::class) class BrokerDiscoveryClientTests { + companion object { + /** + * Mirrors ClientActiveBrokerCache.BROKER_METADATA_CACHE_STORE_ON_BROKER_SDK_SIDE_STORAGE_NAME + * (which is private). Centralized here to reduce drift if the production name changes. + */ + private const val BROKER_SDK_CACHE_FILE_NAME = "BROKER_METADATA_CACHE_STORE_ON_BROKER_SDK_SIDE" + } + /** * Happy scenario. * - First time querying (nothing in the cache). @@ -1037,100 +1045,108 @@ class BrokerDiscoveryClientTests { // Ensure test isolation: clear any leftover data in the broker SDK cache SharedPreferences // and the SharedPreferencesFileManager singleton cache. - context.getSharedPreferences("BROKER_METADATA_CACHE_STORE_ON_BROKER_SDK_SIDE", Context.MODE_PRIVATE) + context.getSharedPreferences(BROKER_SDK_CACHE_FILE_NAME, Context.MODE_PRIVATE) .edit().clear().commit() SharedPreferencesFileManager.clearSingletonCache() AuthenticationSettings.INSTANCE.clearSecretKeysForTestCases() - // A mock "keystore" key provider using a different key AND a different identifier than - // the predefined one. PredefinedKeyProvider always uses "U001", so we need a custom - // ISecretKeyProvider with a distinct identifier (e.g. "KS01") to simulate the real - // KeyStoreBackedSecretKeyProvider behavior. - val mockKeystoreKeyProvider = object : ISecretKeyProvider { - override val alias: String = "MOCK_KEYSTORE_KEY" - override val keyTypeIdentifier: String = "KS01" - override val key: javax.crypto.SecretKey = AES256SecretKeyGenerator.generateKeyFromRawBytes(MockData.ANOTHER_PREDEFINED_KEY) - override val cipherTransformation: String = "AES/CBC/PKCS5Padding" - } - - // Step 1: Write cache data using the predefined key (simulates MSAL-initialized path). - AuthenticationSettings.INSTANCE.setSecretKey(MockData.PREDEFINED_KEY) - val writeEncryptionManager = AndroidAuthSdkStorageEncryptionManager(context) - val writeSupplier = AndroidStorageSupplier(context, writeEncryptionManager) - val writeCache = ClientActiveBrokerCache.getBrokerSdkCache(writeSupplier) - writeCache.setCachedActiveBroker(prodMicrosoftAuthenticator) - - // Verify data was written successfully. - Assert.assertEquals(prodMicrosoftAuthenticator, writeCache.getCachedActiveBroker()) - - // Step 2: Clear the predefined key and SharedPreferencesFileManager cache - // (simulates app restart where MSAL hasn't initialized). - AuthenticationSettings.INSTANCE.clearSecretKeysForTestCases() - SharedPreferencesFileManager.clearSingletonCache() - - // Step 3: Create a "keystore-only" encryption manager that uses the mock keystore key - // and throws ClientException for U001-encrypted data (simulates the fix). - val readEncryptionManager = object : StorageEncryptionManager() { - override fun getKeyProviderForEncryption(): ISecretKeyProvider { - return mockKeystoreKeyProvider + try { + // A mock "keystore" key provider using a different key AND a different identifier than + // the predefined one. PredefinedKeyProvider always uses "U001", so we need a custom + // ISecretKeyProvider with a distinct identifier (e.g. "KS01") to simulate the real + // KeyStoreBackedSecretKeyProvider behavior. + val mockKeystoreKeyProvider = object : ISecretKeyProvider { + override val alias: String = "MOCK_KEYSTORE_KEY" + override val keyTypeIdentifier: String = "KS01" + override val key: javax.crypto.SecretKey = AES256SecretKeyGenerator.generateKeyFromRawBytes(MockData.ANOTHER_PREDEFINED_KEY) + override val cipherTransformation: String = "AES/CBC/PKCS5Padding" } - override fun getKeyProviderForDecryption(cipherText: ByteArray): List { - val keyIdentifier = getKeyIdentifierFromCipherText(cipherText) - if (PredefinedKeyProvider.USER_PROVIDED_KEY_IDENTIFIER.equals(keyIdentifier, ignoreCase = true)) { - throw ClientException( - ErrorStrings.DECRYPTION_FAILED, - "Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, " + - "but mPredefinedKeyProvider is null." - ) + // Step 1: Write cache data using the predefined key (simulates MSAL-initialized path). + AuthenticationSettings.INSTANCE.setSecretKey(MockData.PREDEFINED_KEY) + val writeEncryptionManager = AndroidAuthSdkStorageEncryptionManager(context) + val writeSupplier = AndroidStorageSupplier(context, writeEncryptionManager) + val writeCache = ClientActiveBrokerCache.getBrokerSdkCache(writeSupplier) + writeCache.setCachedActiveBroker(prodMicrosoftAuthenticator) + + // Verify data was written successfully. + Assert.assertEquals(prodMicrosoftAuthenticator, writeCache.getCachedActiveBroker()) + + // Step 2: Clear the predefined key and SharedPreferencesFileManager cache + // (simulates app restart where MSAL hasn't initialized). + AuthenticationSettings.INSTANCE.clearSecretKeysForTestCases() + SharedPreferencesFileManager.clearSingletonCache() + + // Step 3: Create a "keystore-only" encryption manager that uses the mock keystore key + // and throws ClientException for U001-encrypted data (simulates the fix). + val readEncryptionManager = object : StorageEncryptionManager() { + override fun getKeyProviderForEncryption(): ISecretKeyProvider { + return mockKeystoreKeyProvider } - // For data encrypted with the mock keystore key ("KS01"), return it. - return listOf(mockKeystoreKeyProvider) - } - } - val readSupplier = AndroidStorageSupplier(context, readEncryptionManager) - val readCache = ClientActiveBrokerCache.getBrokerSdkCache(readSupplier) - // Step 4: Use this cache in BrokerDiscoveryClient — should fall back to IPC, not crash. - val client = BrokerDiscoveryClient( - brokerCandidates = setOf( - prodMicrosoftAuthenticator, prodCompanyPortal - ), - getActiveBrokerFromAccountManager = { - throw IllegalStateException("Should not fall back to AccountManager when IPC succeeds") - }, - ipcStrategy = object : IIpcStrategy { - override fun communicateToBroker(bundle: BrokerOperationBundle): Bundle { - val returnBundle = Bundle() - returnBundle.putString( - BrokerDiscoveryClient.ACTIVE_BROKER_PACKAGE_NAME_BUNDLE_KEY, - prodCompanyPortal.packageName - ) - returnBundle.putString( - BrokerDiscoveryClient.ACTIVE_BROKER_SIGNING_CERTIFICATE_THUMBPRINT_BUNDLE_KEY, - prodCompanyPortal.signingCertificateThumbprint - ) - return returnBundle - } - override fun isSupportedByTargetedBroker(targetedBrokerPackageName: String): Boolean { - return true - } - override fun getType(): IIpcStrategy.Type { - return IIpcStrategy.Type.CONTENT_PROVIDER + override fun getKeyProviderForDecryption(cipherText: ByteArray): List { + val keyIdentifier = getKeyIdentifierFromCipherText(cipherText) + if (PredefinedKeyProvider.USER_PROVIDED_KEY_IDENTIFIER.equals(keyIdentifier, ignoreCase = true)) { + throw ClientException( + ErrorStrings.DECRYPTION_FAILED, + "Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, " + + "but mPredefinedKeyProvider is null." + ) + } + // For data encrypted with the mock keystore key ("KS01"), return it. + return listOf(mockKeystoreKeyProvider) } - }, - cache = readCache, - isPackageInstalled = { - it == prodMicrosoftAuthenticator || it == prodCompanyPortal - }, - isValidBroker = { true } - ) - - // Should NOT crash — should fall back to IPC and discover Company Portal. - val result = client.getActiveBroker() - Assert.assertEquals(prodCompanyPortal, result) - - // After IPC re-populates the cache, it should be readable with the mock keystore key. - Assert.assertEquals(prodCompanyPortal, readCache.getCachedActiveBroker()) + } + val readSupplier = AndroidStorageSupplier(context, readEncryptionManager) + val readCache = ClientActiveBrokerCache.getBrokerSdkCache(readSupplier) + + // Step 4: Use this cache in BrokerDiscoveryClient — should fall back to IPC, not crash. + val client = BrokerDiscoveryClient( + brokerCandidates = setOf( + prodMicrosoftAuthenticator, prodCompanyPortal + ), + getActiveBrokerFromAccountManager = { + throw IllegalStateException("Should not fall back to AccountManager when IPC succeeds") + }, + ipcStrategy = object : IIpcStrategy { + override fun communicateToBroker(bundle: BrokerOperationBundle): Bundle { + val returnBundle = Bundle() + returnBundle.putString( + BrokerDiscoveryClient.ACTIVE_BROKER_PACKAGE_NAME_BUNDLE_KEY, + prodCompanyPortal.packageName + ) + returnBundle.putString( + BrokerDiscoveryClient.ACTIVE_BROKER_SIGNING_CERTIFICATE_THUMBPRINT_BUNDLE_KEY, + prodCompanyPortal.signingCertificateThumbprint + ) + return returnBundle + } + override fun isSupportedByTargetedBroker(targetedBrokerPackageName: String): Boolean { + return true + } + override fun getType(): IIpcStrategy.Type { + return IIpcStrategy.Type.CONTENT_PROVIDER + } + }, + cache = readCache, + isPackageInstalled = { + it == prodMicrosoftAuthenticator || it == prodCompanyPortal + }, + isValidBroker = { true } + ) + + // Should NOT crash — should fall back to IPC and discover Company Portal. + val result = client.getActiveBroker() + Assert.assertEquals(prodCompanyPortal, result) + + // After IPC re-populates the cache, it should be readable with the mock keystore key. + Assert.assertEquals(prodCompanyPortal, readCache.getCachedActiveBroker()) + } finally { + // Restore global state to avoid leaking into other tests. + context.getSharedPreferences(BROKER_SDK_CACHE_FILE_NAME, Context.MODE_PRIVATE) + .edit().clear().commit() + SharedPreferencesFileManager.clearSingletonCache() + AuthenticationSettings.INSTANCE.clearSecretKeysForTestCases() + } } } \ No newline at end of file From c1ded9ea6a7c6c1c619aadc25bb13e1fbb9bb801 Mon Sep 17 00:00:00 2001 From: Dome Pongmongkol Date: Tue, 14 Apr 2026 15:34:03 -0700 Subject: [PATCH 8/9] copilot --- .../crypto/AndroidAuthSdkStorageEncryptionManagerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java b/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java index f431499203..6f6e920113 100644 --- a/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java +++ b/common/src/test/java/com/microsoft/identity/common/crypto/AndroidAuthSdkStorageEncryptionManagerTest.java @@ -36,6 +36,7 @@ import com.microsoft.identity.common.java.crypto.key.KeyUtil; import com.microsoft.identity.common.java.crypto.key.PredefinedKeyProvider; import com.microsoft.identity.common.java.exception.ClientException; +import com.microsoft.identity.common.java.exception.ErrorStrings; import org.junit.Assert; import org.junit.Before; @@ -118,9 +119,8 @@ public void testGetDecryptionKey_ForDataEncryptedWithPreDefinedKey() throws Clie final List keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); Assert.fail("Expected ClientException"); } catch (ClientException ex) { - Assert.assertEquals( - "Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, but mPredefinedKeyProvider is null.", - ex.getMessage()); + Assert.assertEquals(ErrorStrings.DECRYPTION_FAILED, ex.getErrorCode()); + Assert.assertTrue(ex.getMessage().contains("mPredefinedKeyProvider is null")); } } From a7a5d83b34e222c18f5f2685e5349113ecd765ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:00:38 +0000 Subject: [PATCH 9/9] Rename createFromContextWithKeystoreOnlyEncryption to createFromContextWithKeystoreOnlyEncryptionForStorage Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/0c880240-e3d1-480e-a646-012e0df956d3 Co-authored-by: rpdome <19558668+rpdome@users.noreply.github.com> --- .../common/components/AndroidPlatformComponentsFactory.java | 2 +- .../api/DeviceRegistrationClientApplication.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java b/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java index 3d7a96a8ba..aecbd8c8a6 100644 --- a/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java +++ b/common/src/main/java/com/microsoft/identity/common/components/AndroidPlatformComponentsFactory.java @@ -131,7 +131,7 @@ private static IPlatformComponents create(@NonNull final Context context, * * @param context an application context. **/ - public static IPlatformComponents createFromContextWithKeystoreOnlyEncryption( + public static IPlatformComponents createFromContextWithKeystoreOnlyEncryptionForStorage( @NonNull final Context context) { initializeGlobalStates(context); diff --git a/common/src/main/java/com/microsoft/identity/deviceregistration/api/DeviceRegistrationClientApplication.kt b/common/src/main/java/com/microsoft/identity/deviceregistration/api/DeviceRegistrationClientApplication.kt index 36d081b2c5..e54aad4a9a 100644 --- a/common/src/main/java/com/microsoft/identity/deviceregistration/api/DeviceRegistrationClientApplication.kt +++ b/common/src/main/java/com/microsoft/identity/deviceregistration/api/DeviceRegistrationClientApplication.kt @@ -85,7 +85,7 @@ class DeviceRegistrationClientApplication { */ @Throws(ClientException::class) constructor(context: Context) { - val components = AndroidPlatformComponentsFactory.createFromContextWithKeystoreOnlyEncryption(context) + val components = AndroidPlatformComponentsFactory.createFromContextWithKeystoreOnlyEncryptionForStorage(context) mController = buildController( context, components,