Skip to content
Merged
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
vNext
----------
- [PATCH] Fix: WPJ's BrokerDiscovery cache crash due to shared predefined encryption key with MSAL (#3081)
- [PATCH] Fix ABBA deadlock between AzureActiveDirectory and AzureActiveDirectoryAuthority class monitors by extracting polymorphic getAuthorityURL() calls outside synchronized scopes and removing unnecessary synchronized from ConcurrentHashMap read-only methods (#3082)
- [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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,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#setSecretKey(byte[])}.
* <p>
* This is intended for components (e.g. Device Registration API, Broker API)
* that should not depend on the MSAL/ADAL predefined key lifecycle.
Comment thread
rpdome marked this conversation as resolved.
*
* @param context an application context.
**/
public static IPlatformComponents createFromContextWithKeystoreOnlyEncryptionForStorage(
@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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,7 +59,23 @@ 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}.
* 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) {
if (useKeystoreOnly || AuthenticationSettings.INSTANCE.getSecretKeyData() == null) {
mPredefinedKeyProvider = null;
Comment thread
rpdome marked this conversation as resolved.
} else {
mPredefinedKeyProvider = new PredefinedKeyProvider("USER_DEFINED_KEY",
Expand All @@ -83,15 +101,15 @@ public ISecretKeyProvider getKeyProviderForEncryption() {

@Override
@NonNull
public List<ISecretKeyProvider> getKeyProviderForDecryption(byte[] cipherText) {
final String methodTag = TAG + ":getKeyLoaderForDecryption";
public List<ISecretKeyProvider> getKeyProviderForDecryption(byte[] cipherText) throws ClientException {
final String methodTag = TAG + ":getKeyProviderForDecryption";

final String keyIdentifier = getKeyIdentifierFromCipherText(cipherText);
if (PredefinedKeyProvider.USER_PROVIDED_KEY_IDENTIFIER.equalsIgnoreCase(keyIdentifier)) {
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.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class DeviceRegistrationClientApplication {
*/
@Throws(ClientException::class)
constructor(context: Context) {
val components = AndroidPlatformComponentsFactory.createFromContext(context)
val components = AndroidPlatformComponentsFactory.createFromContextWithKeystoreOnlyEncryptionForStorage(context)
mController = buildController(
context,
components,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
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 com.microsoft.identity.common.java.exception.ErrorStrings;

import org.junit.Assert;
import org.junit.Before;
Expand Down Expand Up @@ -82,7 +84,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<ISecretKeyProvider> keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY);

Expand All @@ -96,7 +98,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<ISecretKeyProvider> keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY);
Expand All @@ -111,18 +113,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<ISecretKeyProvider> keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY);
} catch (IllegalStateException ex) {
Assert.assertEquals(
"Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, but mPredefinedKeyProvider is null.",
ex.getMessage());
Assert.fail("Expected ClientException");
} catch (ClientException ex) {
Assert.assertEquals(ErrorStrings.DECRYPTION_FAILED, ex.getErrorCode());
Assert.assertTrue(ex.getMessage().contains("mPredefinedKeyProvider is null"));
}
}

public void testGetDecryptionKey_ForUnencryptedText_returns_empty_keyprovider() {
@Test
public void testGetDecryptionKey_ForUnencryptedText_returns_empty_keyprovider() throws ClientException {
AuthenticationSettings.INSTANCE.setIgnoreKeyProviderNotFoundError(false);
final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context);
final List<ISecretKeyProvider> keyproviderList = manager.getKeyProviderForDecryption("Unencrypted".getBytes(ENCODING_UTF8));
Comment thread
rpdome marked this conversation as resolved.
Expand All @@ -134,7 +137,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<ISecretKeyProvider> keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY);
Expand All @@ -143,4 +146,83 @@ 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,
* 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<ISecretKeyProvider> keyproviderList =
manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY);

Assert.assertEquals(1, keyproviderList.size());
Assert.assertTrue(keyproviderList.get(0) instanceof KeyStoreBackedSecretKeyProvider);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Comment thread
rpdome marked this conversation as resolved.
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};
Comment thread
rpdome marked this conversation as resolved.

// 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};
Expand Down
Loading
Loading