diff --git a/changelog.txt b/changelog.txt index 2cd9879110..8accfd8231 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [MINOR] Replace AbstractSecretKeyLoader with ISecretKeyProvider (#2666) - [MINOR] Update IP phone app teams signature constants to use SHA-512 format (#2700) - [MINOR] Updating handling of ssl error received in Android WebView's onReceivedSslError callback (#2691) - [MINOR] Fixing the sign in screens when edge to edge is enabled (#2665) diff --git a/common/src/androidTest/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyLoaderTest.java b/common/src/androidTest/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyProviderTest.java similarity index 74% rename from common/src/androidTest/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyLoaderTest.java rename to common/src/androidTest/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyProviderTest.java index 177e011c89..e23cd4adb2 100644 --- a/common/src/androidTest/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyLoaderTest.java +++ b/common/src/androidTest/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyProviderTest.java @@ -29,7 +29,6 @@ import com.microsoft.identity.common.adal.internal.AuthenticationSettings; import com.microsoft.identity.common.internal.util.AndroidKeyStoreUtil; -import com.microsoft.identity.common.java.crypto.key.AES256KeyLoader; import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.util.FileUtil; @@ -50,13 +49,15 @@ import static com.microsoft.identity.common.java.exception.ClientException.INVALID_KEY; -public class AndroidWrappedKeyLoaderTest { +public class AndroidWrappedKeyProviderTest { final Context context = ApplicationProvider.getApplicationContext(); final String MOCK_KEY_ALIAS = "MOCK_KEY_ALIAS"; final String MOCK_KEY_FILE_PATH = "MOCK_KEY_FILE_PATH"; final int TEST_LOOP = 100; + final String AES_ALGORITHM = "AES"; + @Before public void setUp() throws Exception { // Everything is on clean slate. @@ -110,23 +111,23 @@ private AlgorithmParameterSpec getMockKeyPairGeneratorSpec(final String alias) { @Test public void testGenerateKey() throws ClientException { - final AndroidWrappedKeyLoader keyLoader = new AndroidWrappedKeyLoader(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); - final SecretKey secretKey = keyLoader.generateRandomKey(); + final AndroidWrappedKeyProvider keyProvider = new AndroidWrappedKeyProvider(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); + final SecretKey secretKey = keyProvider.generateRandomKey(); - Assert.assertEquals(AES256KeyLoader.AES_ALGORITHM, secretKey.getAlgorithm()); + Assert.assertEquals(AES_ALGORITHM, secretKey.getAlgorithm()); } @Test public void testReadKeyDirectly() throws ClientException { - final AndroidWrappedKeyLoader keyLoader = initKeyLoaderWithKeyEntry(); - final SecretKey secretKey = keyLoader.getKey(); - final SecretKey storedSecretKey = keyLoader.readSecretKeyFromStorage(); + final AndroidWrappedKeyProvider keyProvider = initkeyProviderWithKeyEntry(); + final SecretKey secretKey = keyProvider.getKey(); + final SecretKey storedSecretKey = keyProvider.readSecretKeyFromStorage(); // They're not the same object! Assert.assertNotSame(secretKey, storedSecretKey); - Assert.assertEquals(AES256KeyLoader.AES_ALGORITHM, secretKey.getAlgorithm()); - Assert.assertEquals(AES256KeyLoader.AES_ALGORITHM, storedSecretKey.getAlgorithm()); + Assert.assertEquals(AES_ALGORITHM, secretKey.getAlgorithm()); + Assert.assertEquals(AES_ALGORITHM, storedSecretKey.getAlgorithm()); Assert.assertNotNull(secretKey.getEncoded()); Assert.assertNotNull(storedSecretKey.getEncoded()); @@ -138,14 +139,14 @@ public void testReadKeyDirectly() throws ClientException { public void testLoadKey() throws ClientException { // Nothing exists. This load key function should generate a key if the key hasn't exist. Assert.assertNull(AndroidKeyStoreUtil.readKey(MOCK_KEY_ALIAS)); - Assert.assertNull(FileUtil.readFromFile(getKeyFile(), AndroidWrappedKeyLoader.KEY_FILE_SIZE)); + Assert.assertNull(FileUtil.readFromFile(getKeyFile(), AndroidWrappedKeyProvider.KEY_FILE_SIZE)); - final AndroidWrappedKeyLoader keyLoader = new AndroidWrappedKeyLoader(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); - final SecretKey secretKey = keyLoader.getKey(); + final AndroidWrappedKeyProvider keyProvider = new AndroidWrappedKeyProvider(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); + final SecretKey secretKey = keyProvider.getKey(); - final SecretKey key = keyLoader.getKeyCache().getData(); + final SecretKey key = keyProvider.getKeyCache().getData(); Assert.assertNotNull(key); - Assert.assertEquals(AES256KeyLoader.AES_ALGORITHM, secretKey.getAlgorithm()); + Assert.assertEquals(AES_ALGORITHM, secretKey.getAlgorithm()); Assert.assertArrayEquals(secretKey.getEncoded(), key.getEncoded()); Assert.assertEquals(secretKey.getFormat(), key.getFormat()); } @@ -153,10 +154,10 @@ public void testLoadKey() throws ClientException { @Test public void testLoadKeyFromCorruptedFile_TruncatedExisingKey() throws ClientException { // Create a new Keystore-wrapped key. - final AndroidWrappedKeyLoader keyLoader = new AndroidWrappedKeyLoader(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); - keyLoader.generateRandomKey(); + final AndroidWrappedKeyProvider keyProvider = new AndroidWrappedKeyProvider(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); + keyProvider.generateRandomKey(); - final byte[] wrappedKey = FileUtil.readFromFile(getKeyFile(), AndroidWrappedKeyLoader.KEY_FILE_SIZE); + final byte[] wrappedKey = FileUtil.readFromFile(getKeyFile(), AndroidWrappedKeyProvider.KEY_FILE_SIZE); Assert.assertNotNull(wrappedKey); // Overwrite the key file with corrupted data. @@ -164,7 +165,7 @@ public void testLoadKeyFromCorruptedFile_TruncatedExisingKey() throws ClientExce // It should fail to read, with an exception, and everything should be wiped. try{ - keyLoader.readSecretKeyFromStorage(); + keyProvider.readSecretKeyFromStorage(); Assert.fail(); } catch (ClientException e){ Assert.assertEquals(INVALID_KEY, e.getErrorCode()); @@ -174,16 +175,16 @@ public void testLoadKeyFromCorruptedFile_TruncatedExisingKey() throws ClientExce Assert.assertFalse(getKeyFile().exists()); // the next read should be unblocked. - Assert.assertNull(keyLoader.readSecretKeyFromStorage()); + Assert.assertNull(keyProvider.readSecretKeyFromStorage()); } @Test public void testLoadKeyFromCorruptedFile_InjectGarbage() throws ClientException { // Create a new Keystore-wrapped key. - final AndroidWrappedKeyLoader keyLoader = new AndroidWrappedKeyLoader(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); - keyLoader.generateRandomKey(); + final AndroidWrappedKeyProvider keyProvider = new AndroidWrappedKeyProvider(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); + keyProvider.generateRandomKey(); - final byte[] wrappedKey = FileUtil.readFromFile(getKeyFile(), AndroidWrappedKeyLoader.KEY_FILE_SIZE); + final byte[] wrappedKey = FileUtil.readFromFile(getKeyFile(), AndroidWrappedKeyProvider.KEY_FILE_SIZE); Assert.assertNotNull(wrappedKey); // Overwrite the key file with corrupted data. @@ -191,7 +192,7 @@ public void testLoadKeyFromCorruptedFile_InjectGarbage() throws ClientException // It should fail to read, with an exception, and everything should be wiped. try{ - keyLoader.readSecretKeyFromStorage(); + keyProvider.readSecretKeyFromStorage(); Assert.fail(); } catch (ClientException e){ Assert.assertEquals(INVALID_KEY, e.getErrorCode()); @@ -201,18 +202,18 @@ public void testLoadKeyFromCorruptedFile_InjectGarbage() throws ClientException Assert.assertFalse(getKeyFile().exists()); // the next read should be unblocked. - Assert.assertNull(keyLoader.readSecretKeyFromStorage()); + Assert.assertNull(keyProvider.readSecretKeyFromStorage()); } // 1s With Google Pixel XL, OS Version 29 (100 loop) @Test @Ignore public void testPerf_WithCachedKey() throws ClientException { - final AndroidWrappedKeyLoader keyLoader = new AndroidWrappedKeyLoader(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); + final AndroidWrappedKeyProvider keyProvider = new AndroidWrappedKeyProvider(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); long timeStartLoop = System.nanoTime(); for (int i = 0; i < TEST_LOOP; i++) { - keyLoader.getKey(); + keyProvider.getKey(); } long timeFinishLoop = System.nanoTime(); @@ -223,12 +224,12 @@ public void testPerf_WithCachedKey() throws ClientException { @Test @Ignore public void testPerf_NoCachedKey() throws ClientException { - final AndroidWrappedKeyLoader keyLoader = new AndroidWrappedKeyLoader(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); + final AndroidWrappedKeyProvider keyProvider = new AndroidWrappedKeyProvider(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); long timeStartLoopNotCached = System.nanoTime(); for (int i = 0; i < 100; i++) { - keyLoader.getKeyCache().clear(); - keyLoader.getKey(); + keyProvider.getKeyCache().clear(); + keyProvider.getKey(); } long timeFinishLoopNotCached = System.nanoTime(); @@ -240,31 +241,31 @@ public void testPerf_NoCachedKey() throws ClientException { */ @Test public void testLoadDeletedKeyStoreKey() throws ClientException { - final AndroidWrappedKeyLoader keyLoader = initKeyLoaderWithKeyEntry(); + final AndroidWrappedKeyProvider keyProvider = initkeyProviderWithKeyEntry(); AndroidKeyStoreUtil.deleteKey(MOCK_KEY_ALIAS); // Cached key also be wiped. - final SecretKey key = keyLoader.getKeyCache().getData(); + final SecretKey key = keyProvider.getKeyCache().getData(); Assert.assertNull(key); } @Test public void testLoadDeletedKeyFile() throws ClientException { - final AndroidWrappedKeyLoader keyLoader = initKeyLoaderWithKeyEntry(); + final AndroidWrappedKeyProvider keyProvider = initkeyProviderWithKeyEntry(); FileUtil.deleteFile(getKeyFile()); // Cached key also be wiped. - final SecretKey key = keyLoader.getKeyCache().getData(); + final SecretKey key = keyProvider.getKeyCache().getData(); Assert.assertNull(key); } - private AndroidWrappedKeyLoader initKeyLoaderWithKeyEntry() throws ClientException { - final AndroidWrappedKeyLoader keyLoader = new AndroidWrappedKeyLoader(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); - final SecretKey key = keyLoader.getKey(); + private AndroidWrappedKeyProvider initkeyProviderWithKeyEntry() throws ClientException { + final AndroidWrappedKeyProvider keyProvider = new AndroidWrappedKeyProvider(MOCK_KEY_ALIAS, MOCK_KEY_FILE_PATH, context); + final SecretKey key = keyProvider.getKey(); Assert.assertNotNull(key); - Assert.assertNotNull(keyLoader.getKeyCache().getData()); - return keyLoader; + Assert.assertNotNull(keyProvider.getKeyCache().getData()); + return keyProvider; } } diff --git a/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationSettings.java b/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationSettings.java index caeab333b3..bae91ee797 100644 --- a/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationSettings.java +++ b/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationSettings.java @@ -414,7 +414,7 @@ public boolean getDisableWebViewHardwareAcceleration() { * @param shouldIgnore if true, ignores keyloader not found errors */ @SuppressFBWarnings(ME_ENUM_FIELD_SETTER) - public void setIgnoreKeyLoaderNotFoundError(boolean shouldIgnore) { + public void setIgnoreKeyProviderNotFoundError(boolean shouldIgnore) { mIgnoreKeyLoaderNotFoundError = shouldIgnore; } 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 20e091cbd2..55a9e1b6ec 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 @@ -26,9 +26,8 @@ import com.microsoft.identity.common.adal.internal.AuthenticationSettings; import com.microsoft.identity.common.java.crypto.StorageEncryptionManager; -import com.microsoft.identity.common.java.crypto.key.AES256KeyLoader; -import com.microsoft.identity.common.java.crypto.key.AbstractSecretKeyLoader; -import com.microsoft.identity.common.java.crypto.key.PredefinedKeyLoader; +import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider; +import com.microsoft.identity.common.java.crypto.key.PredefinedKeyProvider; import com.microsoft.identity.common.logging.Logger; import java.util.Collections; @@ -54,49 +53,50 @@ public class AndroidAuthSdkStorageEncryptionManager extends StorageEncryptionMan */ public static final String WRAPPED_KEY_FILE_NAME = "adalks"; - private final PredefinedKeyLoader mPredefinedKeyLoader; - private final AndroidWrappedKeyLoader mKeyStoreKeyLoader; + private final PredefinedKeyProvider mPredefinedKeyProvider; + private final ISecretKeyProvider mKeyStoreKeyProvider; public AndroidAuthSdkStorageEncryptionManager(@NonNull final Context context) { if (AuthenticationSettings.INSTANCE.getSecretKeyData() == null) { - mPredefinedKeyLoader = null; + mPredefinedKeyProvider = null; } else { - mPredefinedKeyLoader = new PredefinedKeyLoader("USER_DEFINED_KEY", + mPredefinedKeyProvider = new PredefinedKeyProvider("USER_DEFINED_KEY", AuthenticationSettings.INSTANCE.getSecretKeyData()); } - mKeyStoreKeyLoader = new AndroidWrappedKeyLoader( + mKeyStoreKeyProvider = new AndroidWrappedKeyProvider( WRAPPING_KEY_ALIAS, WRAPPED_KEY_FILE_NAME, - context); + context + ); } @Override @NonNull - public AES256KeyLoader getKeyLoaderForEncryption() { - if (mPredefinedKeyLoader != null) { - return mPredefinedKeyLoader; + public ISecretKeyProvider getKeyProviderForEncryption() { + if (mPredefinedKeyProvider != null) { + return mPredefinedKeyProvider; } - return mKeyStoreKeyLoader; + return mKeyStoreKeyProvider; } @Override @NonNull - public List getKeyLoaderForDecryption(byte[] cipherText) { + public List getKeyProviderForDecryption(byte[] cipherText) { final String methodTag = TAG + ":getKeyLoaderForDecryption"; final String keyIdentifier = getKeyIdentifierFromCipherText(cipherText); - if (PredefinedKeyLoader.USER_PROVIDED_KEY_IDENTIFIER.equalsIgnoreCase(keyIdentifier)) { - if (mPredefinedKeyLoader != null) { - return Collections.singletonList(mPredefinedKeyLoader); + if (PredefinedKeyProvider.USER_PROVIDED_KEY_IDENTIFIER.equalsIgnoreCase(keyIdentifier)) { + if (mPredefinedKeyProvider != null) { + return Collections.singletonList(mPredefinedKeyProvider); } else { throw new IllegalStateException( "Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, " + - "but mPredefinedKeyLoader is null."); + "but mPredefinedKeyProvider is null."); } - } else if (AndroidWrappedKeyLoader.WRAPPED_KEY_KEY_IDENTIFIER.equalsIgnoreCase(keyIdentifier)) { - return Collections.singletonList(mKeyStoreKeyLoader); + } else if (mKeyStoreKeyProvider.getKeyTypeIdentifier().equalsIgnoreCase(keyIdentifier)) { + return Collections.singletonList(mKeyStoreKeyProvider); } Logger.warn(methodTag, diff --git a/common/src/main/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyLoader.java b/common/src/main/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyProvider.java similarity index 93% rename from common/src/main/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyLoader.java rename to common/src/main/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyProvider.java index 6e4495fdbd..9db8a20e39 100644 --- a/common/src/main/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyLoader.java +++ b/common/src/main/java/com/microsoft/identity/common/crypto/AndroidWrappedKeyProvider.java @@ -22,6 +22,8 @@ // THE SOFTWARE. package com.microsoft.identity.common.crypto; +import static com.microsoft.identity.common.java.crypto.key.AES256SecretKeyGenerator.AES_ALGORITHM; + import android.content.Context; import android.os.Build; import android.security.KeyPairGeneratorSpec; @@ -32,7 +34,8 @@ import com.microsoft.identity.common.internal.util.AndroidKeyStoreUtil; import com.microsoft.identity.common.java.controllers.ExceptionAdapter; -import com.microsoft.identity.common.java.crypto.key.AES256KeyLoader; +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.KeyUtil; import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.flighting.CommonFlight; @@ -71,8 +74,16 @@ * Instead, the actual key that we use to encrypt/decrypt data is 'wrapped/encrypted' with the keystore key * before it get saved to the file. */ -public class AndroidWrappedKeyLoader extends AES256KeyLoader { - private static final String TAG = AndroidWrappedKeyLoader.class.getSimpleName() + "#"; +public class AndroidWrappedKeyProvider implements ISecretKeyProvider { + + /** + * AES is 16 bytes (128 bits), thus PKCS#5 padding should not work, but in + * Java AES/CBC/PKCS5Padding is default(!) algorithm name, thus PKCS5 here + * probably doing PKCS7. We decide to go with Java default string. + */ + private static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding"; + + private static final String TAG = AndroidWrappedKeyProvider.class.getSimpleName() + "#"; /** * Should KeyStore and key file check for validity before every key load be skipped. @@ -135,9 +146,9 @@ public SecretKey getData() { * @param filePath Path to the file for storing the wrapped key. * @param context Android's {@link Context} */ - public AndroidWrappedKeyLoader(@NonNull final String alias, - @NonNull final String filePath, - @NonNull final Context context) { + public AndroidWrappedKeyProvider(@NonNull final String alias, + @NonNull final String filePath, + @NonNull final Context context) { mAlias = alias; mFilePath = filePath; mContext = context; @@ -177,12 +188,11 @@ public synchronized SecretKey getKey() throws ClientException { return key; } - @Override @NonNull protected SecretKey generateRandomKey() throws ClientException { final String methodTag = TAG + ":generateRandomKey"; - final SecretKey key = super.generateRandomKey(); + final SecretKey key = AES256SecretKeyGenerator.INSTANCE.generateRandomKey(); saveSecretKeyToStorage(key); Logger.info(methodTag, "New key is generated with thumbprint: " + @@ -217,7 +227,7 @@ protected SecretKey generateRandomKey() throws ClientException { return null; } - final SecretKey key = AndroidKeyStoreUtil.unwrap(wrappedSecretKey, getKeySpecAlgorithm(), keyPair, WRAP_ALGORITHM); + final SecretKey key = AndroidKeyStoreUtil.unwrap(wrappedSecretKey, AES_ALGORITHM, keyPair, WRAP_ALGORITHM); Logger.info(methodTag, "Key is loaded with thumbprint: " + KeyUtil.getKeyThumbPrint(key)); @@ -259,7 +269,7 @@ private void saveSecretKeyToStorage(@NonNull final SecretKey unencryptedKey) thr if (keyPair == null) { Logger.info(methodTag, "No existing keypair. Generating a new one."); final Span span = OTelUtility.createSpanFromParent(SpanName.KeyPairGeneration.name(), SpanExtension.current().getSpanContext()); - try (final Scope scope = SpanExtension.makeCurrentSpan(span)) { + try (final Scope ignored = SpanExtension.makeCurrentSpan(span)) { keyPair = generateNewKeyPair(); span.setStatus(StatusCode.OK); } catch (final ClientException e) { @@ -329,7 +339,7 @@ private KeyPair generateNewKeyPairAPI23AndAbove() throws ClientException { * Generate a new key pair wrapping key based on legacy logic. Call this for API < 23 or as fallback * until new key gen specs are stable. * @return key pair generated with legacy spec - * @throws ClientException + * @throws ClientException if there is an error generating the key pair. */ @NonNull private KeyPair generateKeyPairWithLegacySpec() throws ClientException{ @@ -464,4 +474,10 @@ private File getKeyFile() { mContext.getDir(mContext.getPackageName(), Context.MODE_PRIVATE), mFilePath); } + + @NonNull + @Override + public String getCipherTransformation() { + return CIPHER_TRANSFORMATION; + } } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/util/AndroidKeyStoreUtil.java b/common/src/main/java/com/microsoft/identity/common/internal/util/AndroidKeyStoreUtil.java index c4092efb33..fa9fe38ab9 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/util/AndroidKeyStoreUtil.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/util/AndroidKeyStoreUtil.java @@ -415,7 +415,8 @@ public static synchronized SecretKey unwrap(@NonNull final byte[] wrappedKeyBlob final Throwable exception; final String errCode; try { - final Cipher wrapCipher = Cipher.getInstance(wrapAlgorithm); + //TODO: Once the new KeyProvider is fully implemented, we can remove this suppression. + final Cipher wrapCipher = Cipher.getInstance(wrapAlgorithm); // CodeQL [SM05136] Used on AndroidWrappedKeyLoader, will be removed once the new KeyProvider is fully implemented. wrapCipher.init(Cipher.UNWRAP_MODE, keyPairForUnwrapping.getPrivate()); return (SecretKey) wrapCipher.unwrap(wrappedKeyBlob, wrappedKeyAlgorithm, Cipher.SECRET_KEY); } catch (final IllegalArgumentException e) { 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 f1e0c4510b..fd29ed29c0 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 @@ -32,9 +32,9 @@ import androidx.test.core.app.ApplicationProvider; import com.microsoft.identity.common.adal.internal.AuthenticationSettings; -import com.microsoft.identity.common.java.crypto.key.AbstractSecretKeyLoader; +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.PredefinedKeyLoader; +import com.microsoft.identity.common.java.crypto.key.PredefinedKeyProvider; import org.junit.Assert; import org.junit.Before; @@ -62,9 +62,9 @@ public void setUp() { public void testGetEncryptionKey() { final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); - final AbstractSecretKeyLoader loader = manager.getKeyLoaderForEncryption(); - Assert.assertTrue(loader instanceof AndroidWrappedKeyLoader); - Assert.assertNotEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(loader)); + final ISecretKeyProvider provider = manager.getKeyProviderForEncryption(); + Assert.assertTrue(provider instanceof AndroidWrappedKeyProvider); + Assert.assertNotEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(provider)); } @Test @@ -72,9 +72,9 @@ public void testGetEncryptionKey_PreDefinedKeyProvided() { AuthenticationSettings.INSTANCE.setSecretKey(PREDEFINED_KEY); final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); - final AbstractSecretKeyLoader loader = manager.getKeyLoaderForEncryption(); - Assert.assertTrue(loader instanceof PredefinedKeyLoader); - Assert.assertEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(loader)); + final ISecretKeyProvider provider = manager.getKeyProviderForEncryption(); + Assert.assertTrue(provider instanceof PredefinedKeyProvider); + Assert.assertEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(provider)); } /** @@ -84,11 +84,11 @@ public void testGetEncryptionKey_PreDefinedKeyProvided() { @Test public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey() { final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); - final List keyLoaderList = manager.getKeyLoaderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY); + final List keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY); - Assert.assertEquals(1, keyLoaderList.size()); - Assert.assertTrue(keyLoaderList.get(0) instanceof AndroidWrappedKeyLoader); - Assert.assertNotEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(keyLoaderList.get(0))); + Assert.assertEquals(1, keyproviderList.size()); + Assert.assertTrue(keyproviderList.get(0) instanceof AndroidWrappedKeyProvider); + Assert.assertNotEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(keyproviderList.get(0))); } /** @@ -99,11 +99,11 @@ public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey() { public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey_PreDefinedKeyProvided() { AuthenticationSettings.INSTANCE.setSecretKey(PREDEFINED_KEY); final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); - final List keyLoaderList = manager.getKeyLoaderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY); + final List keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY); - Assert.assertEquals(1, keyLoaderList.size()); - Assert.assertTrue(keyLoaderList.get(0) instanceof AndroidWrappedKeyLoader); - Assert.assertNotEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(keyLoaderList.get(0))); + Assert.assertEquals(1, keyproviderList.size()); + Assert.assertTrue(keyproviderList.get(0) instanceof AndroidWrappedKeyProvider); + Assert.assertNotEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(keyproviderList.get(0))); } /** @@ -114,19 +114,19 @@ public void testGetDecryptionKey_ForDataEncryptedWithKeyStoreKey_PreDefinedKeyPr public void testGetDecryptionKey_ForDataEncryptedWithPreDefinedKey() { final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); try { - final List keyLoaderList = manager.getKeyLoaderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); + final List keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); } catch (IllegalStateException ex) { Assert.assertEquals( - "Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, but mPredefinedKeyLoader is null.", + "Cipher Text is encrypted by USER_PROVIDED_KEY_IDENTIFIER, but mPredefinedKeyProvider is null.", ex.getMessage()); } } - public void testGetDecryptionKey_ForUnencryptedText_returns_empty_keyloader() { - AuthenticationSettings.INSTANCE.setIgnoreKeyLoaderNotFoundError(false); + public void testGetDecryptionKey_ForUnencryptedText_returns_empty_keyprovider() { + AuthenticationSettings.INSTANCE.setIgnoreKeyProviderNotFoundError(false); final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); - final List keyLoaderList = manager.getKeyLoaderForDecryption("Unencrypted".getBytes(ENCODING_UTF8)); - Assert.assertEquals(0, keyLoaderList.size()); + final List keyproviderList = manager.getKeyProviderForDecryption("Unencrypted".getBytes(ENCODING_UTF8)); + Assert.assertEquals(0, keyproviderList.size()); } /** @@ -137,10 +137,10 @@ public void testGetDecryptionKey_ForUnencryptedText_returns_empty_keyloader() { public void testGetDecryptionKey_ForDataEncryptedWithPreDefinedKey_PreDefinedKeyProvided() { AuthenticationSettings.INSTANCE.setSecretKey(PREDEFINED_KEY); final AndroidAuthSdkStorageEncryptionManager manager = new AndroidAuthSdkStorageEncryptionManager(context); - final List keyLoaderList = manager.getKeyLoaderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); + final List keyproviderList = manager.getKeyProviderForDecryption(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); - Assert.assertEquals(1, keyLoaderList.size()); - Assert.assertTrue(keyLoaderList.get(0) instanceof PredefinedKeyLoader); - Assert.assertEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(keyLoaderList.get(0))); + Assert.assertEquals(1, keyproviderList.size()); + Assert.assertTrue(keyproviderList.get(0) instanceof PredefinedKeyProvider); + Assert.assertEquals(KeyUtil.getKeyThumbPrint(secretKeyMock), KeyUtil.getKeyThumbPrint(keyproviderList.get(0))); } } diff --git a/common/src/test/java/com/microsoft/identity/common/shadows/ShadowAndroidSdkStorageEncryptionManager.java b/common/src/test/java/com/microsoft/identity/common/shadows/ShadowAndroidSdkStorageEncryptionManager.java index 647fef0645..87ce239166 100644 --- a/common/src/test/java/com/microsoft/identity/common/shadows/ShadowAndroidSdkStorageEncryptionManager.java +++ b/common/src/test/java/com/microsoft/identity/common/shadows/ShadowAndroidSdkStorageEncryptionManager.java @@ -23,8 +23,8 @@ package com.microsoft.identity.common.shadows; import com.microsoft.identity.common.crypto.AndroidAuthSdkStorageEncryptionManager; -import com.microsoft.identity.common.java.crypto.key.PredefinedKeyLoader; -import com.microsoft.identity.common.java.crypto.key.AES256KeyLoader; +import com.microsoft.identity.common.java.crypto.key.PredefinedKeyProvider; +import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider; import org.robolectric.annotation.Implements; @@ -35,14 +35,14 @@ public class ShadowAndroidSdkStorageEncryptionManager { final byte[] encryptionKey = 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}; - final AES256KeyLoader mUserDefinedKey = new PredefinedKeyLoader("MOCK_ALIAS", encryptionKey); + final ISecretKeyProvider mUserDefinedKey = new PredefinedKeyProvider("MOCK_ALIAS", encryptionKey); - public AES256KeyLoader getKeyLoaderForEncryption() { + public ISecretKeyProvider getKeyLoaderForEncryption() { return mUserDefinedKey; } - public List getKeyLoaderForDecryption(byte[] cipherText) { - return new ArrayList() {{ + public List getKeyLoaderForDecryption(byte[] cipherText) { + return new ArrayList() {{ add(mUserDefinedKey); }}; } diff --git a/common4j/src/main/com/microsoft/identity/common/java/crypto/StorageEncryptionManager.java b/common4j/src/main/com/microsoft/identity/common/java/crypto/StorageEncryptionManager.java index cd7fcc5dc2..8e1f7ae954 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/crypto/StorageEncryptionManager.java +++ b/common4j/src/main/com/microsoft/identity/common/java/crypto/StorageEncryptionManager.java @@ -38,7 +38,7 @@ import com.microsoft.identity.common.java.base64.Base64Flags; import com.microsoft.identity.common.java.base64.Base64Util; import com.microsoft.identity.common.java.controllers.ExceptionAdapter; -import com.microsoft.identity.common.java.crypto.key.AbstractSecretKeyLoader; +import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider; import com.microsoft.identity.common.java.crypto.key.KeyUtil; import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.exception.ErrorStrings; @@ -88,9 +88,9 @@ public abstract class StorageEncryptionManager implements IKeyAccessor { /** * Length of key identifiers that are appended before the encrypted data. - * See: PredefinedKeyLoader.USER_PROVIDED_KEY_IDENTIFIER, - * AndroidWrappedKeyLoader.WRAPPED_KEY_KEY_IDENTIFIER, - * KeyringKeyLoader.KEY_IDENTIFIER + * See: PredefinedkeyProvider.USER_PROVIDED_KEY_IDENTIFIER, + * AndroidWrappedkeyProvider.WRAPPED_KEY_KEY_IDENTIFIER, + * KeyringkeyProvider.KEY_IDENTIFIER */ public static final int KEY_IDENTIFIER_LENGTH = 4; @@ -128,23 +128,23 @@ public byte[] encrypt(final byte[] plaintext) final Throwable exception; // load key for encryption if not loaded - final AbstractSecretKeyLoader keyLoader = getKeyLoaderForEncryption(); - if (keyLoader == null) { + final ISecretKeyProvider keyProvider = getKeyProviderForEncryption(); + if (keyProvider == null) { // Developer error. Throw. - throw new IllegalStateException("Cannot find a matching Keyloader."); + throw new IllegalStateException("Cannot find a matching keyProvider."); } try { - final SecretKey encryptionKey = keyLoader.getKey(); + final SecretKey encryptionKey = keyProvider.getKey(); final SecretKey encryptionHMACKey = KeyUtil.getHMacKey(encryptionKey); - final byte[] keyIdentifier = keyLoader.getKeyTypeIdentifier().getBytes(ENCODING_UTF8); + final byte[] keyIdentifier = keyProvider.getKeyTypeIdentifier().getBytes(ENCODING_UTF8); // IV: Initialization vector that is needed to start CBC final byte[] iv = mGenerator.generate(); final IvParameterSpec ivSpec = new IvParameterSpec(iv); // Set to encrypt mode - final Cipher cipher = Cipher.getInstance(keyLoader.getCipherAlgorithm()); + final Cipher cipher = Cipher.getInstance(keyProvider.getCipherTransformation()); final Mac mac = Mac.getInstance(HMAC_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, ivSpec); @@ -212,19 +212,19 @@ public byte[] decrypt(final byte[] cipherText) throws ClientException { return cipherText; } - final List keysForDecryption = getKeyLoaderForDecryption(cipherText); + final List keysForDecryption = getKeyProviderForDecryption(cipherText); if (keysForDecryption.size() == 0) { // Developer error. Throw. - throw new IllegalStateException("Cannot find a matching Keyloader."); + throw new IllegalStateException("Cannot find a matching keyProvider."); } final List suppressedException = new ArrayList<>(); - for (final AbstractSecretKeyLoader keyLoader : keysForDecryption) { + for (final ISecretKeyProvider keyProvider : keysForDecryption) { try { - return decryptWithSecretKey(dataBytes, keyLoader); + return decryptWithSecretKey(dataBytes, keyProvider); } catch (final Throwable e) { - Logger.warn(methodTag, "Failed to decrypt with key:" + keyLoader.getAlias() + - " thumbprint : " + KeyUtil.getKeyThumbPrint(keyLoader)); + Logger.warn(methodTag, "Failed to decrypt with key:" + keyProvider.getAlias() + + " thumbprint : " + KeyUtil.getKeyThumbPrint(keyProvider)); suppressedException.add(e); } } @@ -271,31 +271,31 @@ public IKeyAccessor generateDerivedKey(byte[] label, byte[] ctx, CryptoSuite sui } /** - * Decrypted the given encrypted blob with a key from {@link AbstractSecretKeyLoader} + * Decrypted the given encrypted blob with a key from {@link ISecretKeyProvider} * * @param encryptedBlobWithoutEncodeVersion the encrypted blob with the format of * [KeyIdentifier][encryptedData][iv][MACDigest]. - * @param keyLoader a {@link AbstractSecretKeyLoader} to load the decryption key from. + * @param keyProvider a {@link ISecretKeyProvider} to load the decryption key from. */ private byte[] decryptWithSecretKey(final byte[] encryptedBlobWithoutEncodeVersion, - @NonNull final AbstractSecretKeyLoader keyLoader) + @NonNull final ISecretKeyProvider keyProvider) throws ClientException { final String errCode; final Throwable exception; try { - final SecretKey secretKey = keyLoader.getKey(); + final SecretKey secretKey = keyProvider.getKey(); final SecretKey hmacKey = KeyUtil.getHMacKey(secretKey); // byte input array: [keyVersion][encryptedData][IV][macDigest] final int ivIndex = encryptedBlobWithoutEncodeVersion.length - IV_LENGTH - MAC_DIGEST_LENGTH; final int macDigestIndex = encryptedBlobWithoutEncodeVersion.length - MAC_DIGEST_LENGTH; - final int encryptedDataIndex = keyLoader.getKeyTypeIdentifier().getBytes(ENCODING_UTF8).length; + final int encryptedDataIndex = keyProvider.getKeyTypeIdentifier().getBytes(ENCODING_UTF8).length; final int encryptedDataLength = ivIndex - encryptedDataIndex; // Calculate digest again and compare to the appended value // incoming message: version+encryptedData+IV+Digest // Digest of EncryptedData+IV excluding the digest itself. - final Cipher cipher = Cipher.getInstance(keyLoader.getCipherAlgorithm()); + final Cipher cipher = Cipher.getInstance(keyProvider.getCipherTransformation()); final Mac mac = Mac.getInstance(HMAC_ALGORITHM); mac.init(hmacKey); mac.update(encryptedBlobWithoutEncodeVersion, 0, macDigestIndex); @@ -489,10 +489,10 @@ private void assertHMac(final byte[] encryptedBlob, /** * Get Secret Key to use in encryption/decryption. * - * @return a SecretKey loader. + * @return a SecretKey provider. */ @NonNull - public abstract AbstractSecretKeyLoader getKeyLoaderForEncryption() throws ClientException; + public abstract ISecretKeyProvider getKeyProviderForEncryption() throws ClientException; /** * Identify the encrypted blob and return a list of potential candidate key loaders for decryption. @@ -501,5 +501,5 @@ private void assertHMac(final byte[] encryptedBlob, * @return a prioritized list of SecretKey (earlier keys is more likely to be the correct one). **/ @NonNull - abstract public List getKeyLoaderForDecryption(final byte[] cipherText) throws ClientException; + abstract public List getKeyProviderForDecryption(final byte[] cipherText) throws ClientException; } diff --git a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/AES256SecretKeyGenerator.kt b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/AES256SecretKeyGenerator.kt new file mode 100644 index 0000000000..e0f16c87a3 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/AES256SecretKeyGenerator.kt @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.crypto.key + +import com.microsoft.identity.common.java.exception.ClientException +import com.microsoft.identity.common.java.logging.Logger +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +/** + * Implementation of [ISecretKeyGenerator] for AES-256 keys. + * This class provides functionality to generate random AES-256 secret keys or + * create them from raw byte arrays. + */ +object AES256SecretKeyGenerator : ISecretKeyGenerator { + private val TAG = AES256SecretKeyGenerator::class.java.simpleName + const val AES_ALGORITHM = "AES" + const val AES_KEY_SIZE = 256 + + /** + * Generates a random AES-256 secret key. + * Uses [KeyGenerator] to create a cryptographically secure random key. + * + * @return A randomly generated [SecretKey] instance. + * @throws ClientException If the algorithm is not available on the current platform. + */ + @Throws(ClientException::class) + override fun generateRandomKey(): SecretKey { + val methodTag = "$TAG:generateRandomKey" + try { + val keygen = KeyGenerator.getInstance(AES_ALGORITHM) + keygen.init(AES_KEY_SIZE, SecureRandom()) + return keygen.generateKey() + } catch (e: NoSuchAlgorithmException) { + val clientException = ClientException( + ClientException.NO_SUCH_ALGORITHM, + e.message, + e + ) + Logger.error(methodTag, clientException.errorCode, e) + throw clientException + } + } + + /** + * Creates an AES-256 secret key from the provided raw bytes. + * + * @param rawBytes The raw byte array to create the key from. + * @return A [SecretKey] created from the provided raw bytes. + */ + override fun generateKeyFromRawBytes(rawBytes: ByteArray): SecretKey { + return SecretKeySpec(rawBytes, AES_ALGORITHM) + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/AbstractSecretKeyLoader.java b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/AbstractSecretKeyLoader.java deleted file mode 100644 index c9555c2f2d..0000000000 --- a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/AbstractSecretKeyLoader.java +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -package com.microsoft.identity.common.java.crypto.key; - -import com.microsoft.identity.common.java.base64.Base64Flags; -import com.microsoft.identity.common.java.base64.Base64Util; -import com.microsoft.identity.common.java.crypto.StorageEncryptionManager; -import com.microsoft.identity.common.java.exception.ClientException; -import com.microsoft.identity.common.java.logging.Logger; - -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import lombok.NonNull; - -import static com.microsoft.identity.common.java.exception.ClientException.NO_SUCH_ALGORITHM; - -/** - * Abstracts how a {@link SecretKey} is loaded/cached/sourced/used. - */ -public abstract class AbstractSecretKeyLoader { - private static final String TAG = AbstractSecretKeyLoader.class.getSimpleName(); - - /** - * Returns this key's alias/name. - * Each key will have a unique alias/name. - */ - @NonNull - public abstract String getAlias(); - - /** - * Returns the key. - */ - @NonNull - public abstract SecretKey getKey() throws ClientException; - - /** - * Returns the Algorithm of this key. - * This must be compatible with {@link KeyGenerator#getInstance(String)} - */ - @NonNull - protected abstract String getKeySpecAlgorithm(); - - /** - * Returns the size of this key. - * This must be compatible with {@link KeyGenerator#init(int, SecureRandom)} )} - */ - protected abstract int getKeySize(); - - /** - * Gets an identifier of this key type. - * This might be padded into the encrypted string. - */ - @NonNull - public abstract String getKeyTypeIdentifier(); - - /** - * Gets the cipher algorithm that is meant to be used with this key type. - */ - @NonNull - public abstract String getCipherAlgorithm(); - - /** - * Generate a random AES-256 secret key. - * - * @return SecretKey. - */ - @NonNull - protected SecretKey generateRandomKey() throws ClientException { - final String methodName = ":generateRandomKey"; - - try { - final KeyGenerator keygen = KeyGenerator.getInstance(getKeySpecAlgorithm()); - keygen.init(getKeySize(), new SecureRandom()); - return keygen.generateKey(); - } catch (final NoSuchAlgorithmException e) { - final ClientException clientException = new ClientException( - NO_SUCH_ALGORITHM, - e.getMessage(), - e - ); - - Logger.error( - TAG + methodName, - clientException.getErrorCode(), - e - ); - - throw clientException; - } - } - - /** - * Generate a random AES-256 secret key from rawbytes. - *

- * If a non AES-256 rawBytes is provided, this will still return a SecretKey, - * but an exception would be thrown in {@link StorageEncryptionManager} - * during encryption/decryption. - * - * @return SecretKey. - */ - @NonNull - protected SecretKey generateKeyFromRawBytes(@NonNull final byte[] rawBytes) { - return new SecretKeySpec(rawBytes, getKeySpecAlgorithm()); - } - - /** - * Serializes a {@link SecretKey} into a {@link String}. - */ - public String serializeSecretKey(@NonNull final SecretKey key) { - return Base64Util.encodeToString(key.getEncoded(), Base64Flags.DEFAULT); - } - - /** - * Deserializes a {@link String} into a {@link SecretKey}. - */ - public SecretKey deserializeSecretKey(@NonNull final String serializedKey) { - return generateKeyFromRawBytes(Base64Util.decode(serializedKey, Base64Flags.DEFAULT)); - } -} diff --git a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/AES256KeyLoader.java b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/ISecretKeyGenerator.kt similarity index 55% rename from common4j/src/main/com/microsoft/identity/common/java/crypto/key/AES256KeyLoader.java rename to common4j/src/main/com/microsoft/identity/common/java/crypto/key/ISecretKeyGenerator.kt index a6e0cc4b4e..a622f5414e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/AES256KeyLoader.java +++ b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/ISecretKeyGenerator.kt @@ -20,44 +20,32 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -package com.microsoft.identity.common.java.crypto.key; +package com.microsoft.identity.common.java.crypto.key -import lombok.NonNull; - -public abstract class AES256KeyLoader extends AbstractSecretKeyLoader { - private static final String TAG = AES256KeyLoader.class.getSimpleName(); - - /** - * Key size - */ - private static final int KEY_SIZE = 256; +import com.microsoft.identity.common.java.exception.ClientException +import javax.crypto.SecretKey +/** + * Interface for secret key generation. + * Implementations of this interface provide functionality to generate cryptographic + * secret keys either randomly or from raw byte arrays. + */ +interface ISecretKeyGenerator { /** - * Key spec algorithm. + * Generates a cryptographically secure random secret key. + * + * @return A randomly generated [SecretKey] instance. + * @throws ClientException If an error occurs during key generation, + * such as when the algorithm is not available. */ - public static final String AES_ALGORITHM = "AES"; + @Throws(ClientException::class) + fun generateRandomKey(): SecretKey /** - * AES is 16 bytes (128 bits), thus PKCS#5 padding should not work, but in - * Java AES/CBC/PKCS5Padding is default(!) algorithm name, thus PKCS5 here - * probably doing PKCS7. We decide to go with Java default string. + * Creates a secret key from the provided raw bytes. + * + * @param rawBytes The raw byte array to create the key from. + * @return A [SecretKey] created from the provided raw bytes. */ - private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; - - @Override - @NonNull - public String getKeySpecAlgorithm() { - return AES_ALGORITHM; - } - - @Override - @NonNull - public String getCipherAlgorithm(){ - return CIPHER_ALGORITHM; - } - - @Override - protected int getKeySize() { - return KEY_SIZE; - } + fun generateKeyFromRawBytes(rawBytes: ByteArray): SecretKey } diff --git a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/ISecretKeyProvider.kt b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/ISecretKeyProvider.kt new file mode 100644 index 0000000000..5192ed09c3 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/ISecretKeyProvider.kt @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.crypto.key + +import com.microsoft.identity.common.java.exception.ClientException +import javax.crypto.SecretKey + +/** + * Interface defining how a [SecretKey] is loaded, cached, sourced, and used. + * + * [ISecretKeyProvider] provides a consistent abstraction layer for cryptographic key operations + */ +interface ISecretKeyProvider { + /** + * Returns this key's alias or name. + * + * The alias serves as a unique identifier for this key within the system. + * It can be used for key storage, retrieval, and reference across the application. + * Each key implementation must have a unique alias to avoid collisions. + * + * @return The unique key alias as a non-null String. + */ + val alias: String + + /** + * Gets an identifier of this key type. + * + * The key type identifier is used to dy typistinguish between different kees + * in the system. This value might be padded into encrypted strings to indicate + * the key used for encryption, enabling correct key selection during decryption. + * + * @return The key type identifier as a non-null String. + */ + val keyTypeIdentifier: String + + /** + * Gets the cipher transformation string that is meant to be used with this key type. + * + * A cipher transformation string consists of three components: + * - Algorithm: The base cryptographic algorithm (e.g., "AES", "RSA") + * - Mode of operation: How the algorithm should process the data (e.g., "CBC", "GCM", "ECB") + * - Padding scheme: How to handle data that doesn't align with the block size (e.g., "PKCS5Padding", "NoPadding") + * + * For example, "AES/CBC/PKCS5Padding" specifies the AES algorithm in CBC mode with PKCS5 padding. + * The transformation specified must be compatible with the generated keys and supported by the + * security provider being used. + * + * This transformation string is used directly with [javax.crypto.Cipher.getInstance] to create + * the appropriate Cipher object for encryption and decryption operations. + * + * @return The complete cipher transformation string as a non-null String. + */ + val cipherTransformation: String + + /** + * Retrieves the secret key for encryption/decryption operations. + * + * This method handles the loading of an existing key or generation of a new key + * if one doesn't exist. Key storage, caching, and platform-specific logic + * should be encapsulated within implementations of this property. + * + * @return The [SecretKey] to be used for cryptographic operations. + * @throws ClientException If an error occurs during key retrieval or generation, + * including key store access issues or algorithm unavailability. + */ + @get:Throws(ClientException::class) + val key: SecretKey +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/KeyUtil.java b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/KeyUtil.java index 4219d64c88..f9a525df27 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/KeyUtil.java +++ b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/KeyUtil.java @@ -64,12 +64,12 @@ public class KeyUtil { private static final String HMAC_KEYSPEC_ALGORITHM = "AES"; /** - * Derive a thumbprint from the given {@link AbstractSecretKeyLoader}. + * Derive a thumbprint from the given {@link ISecretKeyProvider}. * - * @param keyLoader ISecretKeyLoader to obtain the key (calculate the thumbprint from). + * @param keyLoader ISecretKeyProvider to obtain the key (calculate the thumbprint from). * @return a thumbprint. Will return {@link KeyUtil#UNKNOWN_THUMBPRINT} if it fails to derived one. */ - public static String getKeyThumbPrint(final @NonNull AbstractSecretKeyLoader keyLoader) { + public static String getKeyThumbPrint(final @NonNull ISecretKeyProvider keyLoader) { final String methodName = ":getKeyThumbPrint"; try { return getKeyThumbPrint(keyLoader.getKey()); diff --git a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/PredefinedKeyLoader.java b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/PredefinedKeyProvider.java similarity index 68% rename from common4j/src/main/com/microsoft/identity/common/java/crypto/key/PredefinedKeyLoader.java rename to common4j/src/main/com/microsoft/identity/common/java/crypto/key/PredefinedKeyProvider.java index 25f12832b7..5b936c04fb 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/crypto/key/PredefinedKeyLoader.java +++ b/common4j/src/main/com/microsoft/identity/common/java/crypto/key/PredefinedKeyProvider.java @@ -22,6 +22,10 @@ // THE SOFTWARE. package com.microsoft.identity.common.java.crypto.key; +import com.microsoft.identity.common.java.exception.ClientException; + +import org.jetbrains.annotations.NotNull; + import javax.crypto.SecretKey; import lombok.NonNull; @@ -29,7 +33,13 @@ /** * For loading an AES-256 key from a provided rawbytes array. */ -public class PredefinedKeyLoader extends AES256KeyLoader { +public class PredefinedKeyProvider implements ISecretKeyProvider { + /** + * AES is 16 bytes (128 bits), thus PKCS#5 padding should not work, but in + * Java AES/CBC/PKCS5Padding is default(!) algorithm name, thus PKCS5 here + * probably doing PKCS7. We decide to go with Java default string. + */ + private static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding"; /** * Indicate that the token item is encrypted with the user provided key. @@ -39,27 +49,33 @@ public class PredefinedKeyLoader extends AES256KeyLoader { private final String mAlias; private final SecretKey mKey; - public PredefinedKeyLoader(@NonNull final String alias, - @NonNull final byte[] rawBytes) { + public PredefinedKeyProvider(@NonNull final String alias, + final byte[] rawBytes) { mAlias = alias; - mKey = generateKeyFromRawBytes(rawBytes); + mKey = AES256SecretKeyGenerator.INSTANCE.generateKeyFromRawBytes(rawBytes); } + @NotNull @Override - @NonNull public String getAlias() { return mAlias; } + @NotNull + @Override + public String getKeyTypeIdentifier() { + return USER_PROVIDED_KEY_IDENTIFIER; + } + + @NotNull @Override - @NonNull public SecretKey getKey() { return mKey; } + @NotNull @Override - @NonNull - public String getKeyTypeIdentifier() { - return USER_PROVIDED_KEY_IDENTIFIER; + public String getCipherTransformation() { + return CIPHER_TRANSFORMATION; } } diff --git a/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyUtilTest.java b/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyUtilTest.java index 54ff8b88d1..7c8a7c91ba 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyUtilTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyUtilTest.java @@ -40,7 +40,7 @@ public class KeyUtilTest { @Test public void testThumbprintShouldBeSame() { - final MockAES256KeyLoader firstKeyLoader = new MockAES256KeyLoader(PREDEFINED_KEY, "KEY_1"); + final MockAES256KeyProvider firstKeyLoader = new MockAES256KeyProvider(PREDEFINED_KEY, "KEY_1"); final SecretKey firstKey = firstKeyLoader.getKey(); final String firstThumbPrint = KeyUtil.getKeyThumbPrint(firstKey); @@ -54,10 +54,10 @@ public void testThumbprintShouldBeSame() { @Test public void testThumbprintShouldDifferForDifferentKeys() { - final MockAES256KeyLoader firstKeyLoader = new MockAES256KeyLoader(PREDEFINED_KEY, "KEY_1"); + final MockAES256KeyProvider firstKeyLoader = new MockAES256KeyProvider(PREDEFINED_KEY, "KEY_1"); final SecretKey firstKey = firstKeyLoader.getKey(); - final MockAES256KeyLoader secondKeyLoader = new MockAES256KeyLoader(ANDROID_WRAPPED_KEY, "KEY_2"); + final MockAES256KeyProvider secondKeyLoader = new MockAES256KeyProvider(ANDROID_WRAPPED_KEY, "KEY_2"); final SecretKey secondKey = secondKeyLoader.getKey(); final String firstKeyThumbPrint = KeyUtil.getKeyThumbPrint(firstKey); @@ -69,10 +69,10 @@ public void testThumbprintShouldDifferForDifferentKeys() { @Test public void testThumbprintShouldDifferSlightlyDifferentKeys() { - final MockAES256KeyLoader firstKeyLoader = new MockAES256KeyLoader(PREDEFINED_KEY, "KEY_1"); + final MockAES256KeyProvider firstKeyLoader = new MockAES256KeyProvider(PREDEFINED_KEY, "KEY_1"); final SecretKey firstKey = firstKeyLoader.getKey(); - final MockAES256KeyLoader secondKeyLoader = new MockAES256KeyLoader(PREDEFINED_KEY_SLIGHTLY_MODIFIED, "KEY_2"); + final MockAES256KeyProvider secondKeyLoader = new MockAES256KeyProvider(PREDEFINED_KEY_SLIGHTLY_MODIFIED, "KEY_2"); final SecretKey secondKey = secondKeyLoader.getKey(); final String firstKeyThumbPrint = KeyUtil.getKeyThumbPrint(firstKey); @@ -85,10 +85,10 @@ public void testThumbprintShouldDifferSlightlyDifferentKeys() { @Test public void testThumbprintForSameRawKeyButDifferentKeyObjectShouldBeSame() { - final MockAES256KeyLoader keyLoader = new MockAES256KeyLoader(PREDEFINED_KEY, "KEY_1"); + final MockAES256KeyProvider keyLoader = new MockAES256KeyProvider(PREDEFINED_KEY, "KEY_1"); final SecretKey key = keyLoader.getKey(); - final MockAES256KeyLoader anotherKeyLoader = new MockAES256KeyLoader(PREDEFINED_KEY, "KEY_1"); + final MockAES256KeyProvider anotherKeyLoader = new MockAES256KeyProvider(PREDEFINED_KEY, "KEY_1"); final SecretKey anotherKey = anotherKeyLoader.getKey(); final String thumbPrint = KeyUtil.getKeyThumbPrint(key); diff --git a/common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyLoader.java b/common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyProvider.java similarity index 72% rename from common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyLoader.java rename to common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyProvider.java index 9bdb0aa6d1..b0b90b6b33 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyLoader.java +++ b/common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyProvider.java @@ -22,28 +22,32 @@ // THE SOFTWARE. package com.microsoft.identity.common.java.crypto; -import com.microsoft.identity.common.java.crypto.key.AES256KeyLoader; +import com.microsoft.identity.common.java.crypto.key.AES256SecretKeyGenerator; +import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider; import com.microsoft.identity.common.java.exception.ClientException; + +import org.jetbrains.annotations.NotNull; + import javax.crypto.SecretKey; import lombok.NonNull; -public class MockAES256KeyLoader extends AES256KeyLoader { +public class MockAES256KeyProvider implements ISecretKeyProvider { public static String DEFAULT_MOCK_KEY_IDENTIFIER = "MOCK_ID"; public static String MOCK_ALIAS = "MOCK_ALIAS"; private final SecretKey mKey; private final String mKeyIdentifier; - public MockAES256KeyLoader() throws ClientException { - mKey = generateRandomKey(); + public MockAES256KeyProvider() throws ClientException { + mKey = AES256SecretKeyGenerator.INSTANCE.generateRandomKey(); mKeyIdentifier = DEFAULT_MOCK_KEY_IDENTIFIER; } - public MockAES256KeyLoader(@NonNull final byte[] secretKey, - @NonNull final String keyIdentifier){ - mKey = generateKeyFromRawBytes(secretKey); + public MockAES256KeyProvider(@NonNull final byte[] secretKey, + @NonNull final String keyIdentifier){ + mKey = AES256SecretKeyGenerator.INSTANCE.generateKeyFromRawBytes(secretKey); mKeyIdentifier = keyIdentifier; } @@ -61,4 +65,10 @@ public MockAES256KeyLoader(@NonNull final byte[] secretKey, public @NonNull String getKeyTypeIdentifier() { return mKeyIdentifier; } + + @NotNull + @Override + public String getCipherTransformation() { + return "AES/CBC/PKCS5Padding"; + } } diff --git a/common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyLoaderWithGetKeyError.java b/common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyProviderWithGetKeyError.java similarity index 85% rename from common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyLoaderWithGetKeyError.java rename to common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyProviderWithGetKeyError.java index 3028a5506a..23521500b9 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyLoaderWithGetKeyError.java +++ b/common4j/src/test/com/microsoft/identity/common/java/crypto/MockAES256KeyProviderWithGetKeyError.java @@ -22,14 +22,16 @@ // THE SOFTWARE. package com.microsoft.identity.common.java.crypto; -import com.microsoft.identity.common.java.crypto.key.AES256KeyLoader; +import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider; import com.microsoft.identity.common.java.exception.ClientException; +import org.jetbrains.annotations.NotNull; + import javax.crypto.SecretKey; import lombok.NonNull; -public class MockAES256KeyLoaderWithGetKeyError extends AES256KeyLoader { +public class MockAES256KeyProviderWithGetKeyError implements ISecretKeyProvider { public static String FAIL_TO_LOAD_KEY_ERROR = "FAIL_TO_LOAD_KEY_ERROR"; public static String MOCK_KEY_IDENTIFIER = "MOCK_ERROR_ID"; public static String MOCK_ERROR = "MOCK_ERROR"; @@ -48,4 +50,10 @@ public class MockAES256KeyLoaderWithGetKeyError extends AES256KeyLoader { public @NonNull String getKeyTypeIdentifier() { return MOCK_KEY_IDENTIFIER; } + + @NotNull + @Override + public String getCipherTransformation() { + return "AES/CBC/PKCS5Padding"; + } } diff --git a/common4j/src/test/com/microsoft/identity/common/java/crypto/MockStorageEncryptionManager.java b/common4j/src/test/com/microsoft/identity/common/java/crypto/MockStorageEncryptionManager.java index 9a6f0e1174..501816c7a3 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/crypto/MockStorageEncryptionManager.java +++ b/common4j/src/test/com/microsoft/identity/common/java/crypto/MockStorageEncryptionManager.java @@ -22,7 +22,7 @@ // THE SOFTWARE. package com.microsoft.identity.common.java.crypto; -import com.microsoft.identity.common.java.crypto.key.AbstractSecretKeyLoader; +import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider; import com.microsoft.identity.common.java.exception.ClientException; import java.util.ArrayList; @@ -33,21 +33,21 @@ public class MockStorageEncryptionManager extends StorageEncryptionManager { - private final AbstractSecretKeyLoader mEncryptKey; - private final List mDecryptKey; + private final ISecretKeyProvider mEncryptKey; + private final List mDecryptKey; MockStorageEncryptionManager(@NonNull final byte[] iv, - @Nullable final AbstractSecretKeyLoader key) throws ClientException { + @Nullable final ISecretKeyProvider key) throws ClientException { this(iv, key, - new ArrayList() {{ + new ArrayList() {{ add(key); }}); } MockStorageEncryptionManager(@NonNull final byte[] iv, - @Nullable final AbstractSecretKeyLoader encryptKey, - @Nullable final List decryptKey) throws ClientException { + @Nullable final ISecretKeyProvider encryptKey, + @Nullable final List decryptKey) throws ClientException { super(new IVGenerator() { @Override public byte[] generate() { @@ -59,12 +59,12 @@ public byte[] generate() { } @Override - public @NonNull AbstractSecretKeyLoader getKeyLoaderForEncryption() throws ClientException { + public @NonNull ISecretKeyProvider getKeyProviderForEncryption() throws ClientException { return mEncryptKey; } @Override - public @NonNull List getKeyLoaderForDecryption(@NonNull byte[] cipherText) throws ClientException { + public @NonNull List getKeyProviderForDecryption(@NonNull byte[] cipherText) throws ClientException { return mDecryptKey; } } diff --git a/common4j/src/test/com/microsoft/identity/common/java/crypto/StorageEncryptionManagerTest.java b/common4j/src/test/com/microsoft/identity/common/java/crypto/StorageEncryptionManagerTest.java index e89f96b218..e106d4eb84 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/crypto/StorageEncryptionManagerTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/crypto/StorageEncryptionManagerTest.java @@ -22,8 +22,7 @@ // THE SOFTWARE. package com.microsoft.identity.common.java.crypto; -import com.microsoft.identity.common.java.crypto.key.AES256KeyLoader; -import com.microsoft.identity.common.java.crypto.key.AbstractSecretKeyLoader; +import com.microsoft.identity.common.java.crypto.key.ISecretKeyProvider; import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.exception.ErrorStrings; @@ -40,7 +39,6 @@ import static com.microsoft.identity.common.java.crypto.MockData.PREDEFINED_KEY_MALFORMED; import static com.microsoft.identity.common.java.crypto.MockData.TEXT_ENCRYPTED_BY_PREDEFINED_KEY; import static com.microsoft.identity.common.java.crypto.MockData.TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY; -import static com.microsoft.identity.common.java.crypto.MockData.EXPECTED_ENCRYPTED_TEXT_1_WITH_MALFORMED_ENCODE_VERSION; import static com.microsoft.identity.common.java.crypto.MockData.PREDEFINED_KEY_IV; import static com.microsoft.identity.common.java.crypto.MockData.ANDROID_WRAPPED_KEY_IV; import static com.microsoft.identity.common.java.crypto.MockData.PREDEFINED_KEY_IDENTIFIER; @@ -55,19 +53,19 @@ public class StorageEncryptionManagerTest { @Test public void testEncrypt() throws ClientException { - final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyLoader(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); + final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyProvider(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); Assert.assertArrayEquals(TEXT_ENCRYPTED_BY_PREDEFINED_KEY, manager.encrypt(TEXT_TO_BE_ENCRYPTED_WITH_PREDEFINED_KEY)); - final StorageEncryptionManager manager_2 = new MockStorageEncryptionManager(ANDROID_WRAPPED_KEY_IV, new MockAES256KeyLoader(ANDROID_WRAPPED_KEY, ANDROID_WRAPPED_KEY_IDENTIFIER)); + final StorageEncryptionManager manager_2 = new MockStorageEncryptionManager(ANDROID_WRAPPED_KEY_IV, new MockAES256KeyProvider(ANDROID_WRAPPED_KEY, ANDROID_WRAPPED_KEY_IDENTIFIER)); Assert.assertArrayEquals(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY, manager_2.encrypt(TEXT_TO_BE_ENCRYPTED_WITH_ANDROID_WRAPPED_KEY)); } @Test public void testDecrypt() throws ClientException { - final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyLoader(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); + final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyProvider(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); Assert.assertArrayEquals(TEXT_TO_BE_ENCRYPTED_WITH_PREDEFINED_KEY, manager.decrypt(TEXT_ENCRYPTED_BY_PREDEFINED_KEY)); - final StorageEncryptionManager manager_2 = new MockStorageEncryptionManager(ANDROID_WRAPPED_KEY_IV, new MockAES256KeyLoader(ANDROID_WRAPPED_KEY, ANDROID_WRAPPED_KEY_IDENTIFIER)); + final StorageEncryptionManager manager_2 = new MockStorageEncryptionManager(ANDROID_WRAPPED_KEY_IV, new MockAES256KeyProvider(ANDROID_WRAPPED_KEY, ANDROID_WRAPPED_KEY_IDENTIFIER)); Assert.assertArrayEquals(TEXT_TO_BE_ENCRYPTED_WITH_ANDROID_WRAPPED_KEY, manager_2.decrypt(TEXT_ENCRYPTED_BY_ANDROID_WRAPPED_KEY)); } @@ -88,7 +86,7 @@ public void testDecryptNoKeyLoader() throws ClientException { @Test(expected = RuntimeException.class) public void testDecryptNullKeyLoader() throws ClientException { final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, null, - new ArrayList() {{ + new ArrayList() {{ add(null); }}); manager.decrypt(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); @@ -97,7 +95,7 @@ public void testDecryptNullKeyLoader() throws ClientException { @Test(expected = RuntimeException.class) public void testDecrypt_empty_KeyLoader_throws() throws ClientException { - final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, null, Collections.emptyList()); + final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, null, Collections.emptyList()); manager.decrypt(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); Assert.fail("decrypt() should throw an exception but it succeeds."); } @@ -111,30 +109,30 @@ public void testDecrypt_null_keyloader_throws() throws ClientException { @Test(expected = ClientException.class) public void testEncryptFailToLoadKey() throws ClientException { - final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyLoaderWithGetKeyError()); + final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyProviderWithGetKeyError()); manager.encrypt(TEXT_TO_BE_ENCRYPTED_WITH_PREDEFINED_KEY); Assert.fail(); } @Test public void testDecryptFailToLoadKey() throws ClientException { - final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyLoaderWithGetKeyError()); + final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyProviderWithGetKeyError()); try { manager.decrypt(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); Assert.fail("decrypt() should throw an exception but it succeeds."); } catch (ClientException e){ - Assert.assertEquals(e.getErrorCode(), MockAES256KeyLoaderWithGetKeyError.FAIL_TO_LOAD_KEY_ERROR); + Assert.assertEquals(e.getErrorCode(), MockAES256KeyProviderWithGetKeyError.FAIL_TO_LOAD_KEY_ERROR); } } @Test public void testDecryptFailToLoadOneOfTheKeys() throws ClientException { - final AES256KeyLoader failingKeyLoader = new MockAES256KeyLoaderWithGetKeyError(); - final AES256KeyLoader successKeyLoader = new MockAES256KeyLoader(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER); + final ISecretKeyProvider failingKeyLoader = new MockAES256KeyProviderWithGetKeyError(); + final ISecretKeyProvider successKeyLoader = new MockAES256KeyProvider(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER); // Key order doesn't matter. final StorageEncryptionManager manager_failFirst = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, null, - new ArrayList(){{ + new ArrayList(){{ add(failingKeyLoader); add(successKeyLoader); }}); @@ -142,7 +140,7 @@ public void testDecryptFailToLoadOneOfTheKeys() throws ClientException { Assert.assertArrayEquals(TEXT_TO_BE_ENCRYPTED_WITH_PREDEFINED_KEY, manager_failFirst.decrypt(TEXT_ENCRYPTED_BY_PREDEFINED_KEY)); final StorageEncryptionManager manager_failSecond = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, null, - new ArrayList(){{ + new ArrayList(){{ add(successKeyLoader); add(failingKeyLoader); }}); @@ -152,11 +150,11 @@ public void testDecryptFailToLoadOneOfTheKeys() throws ClientException { @Test public void testDecryptMatchingKeyNotFound() throws ClientException { - final AES256KeyLoader decryptKeyLoader = new MockAES256KeyLoader(); - final AES256KeyLoader decryptKeyLoader_2 = new MockAES256KeyLoader(); + final ISecretKeyProvider decryptKeyLoader = new MockAES256KeyProvider(); + final ISecretKeyProvider decryptKeyLoader_2 = new MockAES256KeyProvider(); final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, null, - new ArrayList(){{ + new ArrayList(){{ add(decryptKeyLoader); add(decryptKeyLoader_2); }}); @@ -173,7 +171,7 @@ public void testDecryptMatchingKeyNotFound() throws ClientException { @Test public void testDecryptWithMalformedKey() { try { - final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyLoader(PREDEFINED_KEY_MALFORMED, PREDEFINED_KEY_IDENTIFIER)); + final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyProvider(PREDEFINED_KEY_MALFORMED, PREDEFINED_KEY_IDENTIFIER)); manager.decrypt(TEXT_ENCRYPTED_BY_PREDEFINED_KEY); Assert.fail("decrypt() should throw an exception but it succeeds."); } catch (final ClientException e){ @@ -183,14 +181,14 @@ public void testDecryptWithMalformedKey() { @Test public void testDecryptUnencryptedText() throws ClientException { - final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyLoader(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); + final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyProvider(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); Assert.assertArrayEquals(TEXT_TO_BE_ENCRYPTED_WITH_PREDEFINED_KEY, manager.decrypt(TEXT_TO_BE_ENCRYPTED_WITH_PREDEFINED_KEY)); } @Test public void testDecryptedTruncatedString() { try { - final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyLoader(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); + final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyProvider(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); final byte[] encryptedByteArray = TEXT_ENCRYPTED_BY_PREDEFINED_KEY; final byte[] truncatedByteArray = Arrays.copyOf(encryptedByteArray, encryptedByteArray.length / 2); manager.decrypt(truncatedByteArray); @@ -200,7 +198,7 @@ public void testDecryptedTruncatedString() { } try { - final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyLoader(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); + final StorageEncryptionManager manager = new MockStorageEncryptionManager(PREDEFINED_KEY_IV, new MockAES256KeyProvider(PREDEFINED_KEY, PREDEFINED_KEY_IDENTIFIER)); manager.decrypt(new String(TEXT_ENCRYPTED_BY_PREDEFINED_KEY, ENCODING_UTF8).substring(0, 25).getBytes(ENCODING_UTF8)); Assert.fail("decrypt() should throw an exception but it succeeds."); } catch (final ClientException e){ diff --git a/common4j/src/test/com/microsoft/identity/common/java/crypto/key/AES256SecretKeyGeneratorTest.kt b/common4j/src/test/com/microsoft/identity/common/java/crypto/key/AES256SecretKeyGeneratorTest.kt new file mode 100644 index 0000000000..f444bdc097 --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/crypto/key/AES256SecretKeyGeneratorTest.kt @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.crypto.key + +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.MockitoAnnotations +import javax.crypto.spec.SecretKeySpec + +/** + * Unit tests for [AES256SecretKeyGenerator]. + */ +class AES256SecretKeyGeneratorTest { + + private lateinit var secretKeyGenerator: AES256SecretKeyGenerator + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + secretKeyGenerator = AES256SecretKeyGenerator + } + + @Test + fun testGenerateRandomKey() { + // Generate a random key + val secretKey = secretKeyGenerator.generateRandomKey() + + // Verify that the key is not null + Assert.assertNotNull(secretKey) + + // Verify that the key algorithm is "AES" + Assert.assertEquals("AES", secretKey.algorithm) + + // Verify that the encoded form of the key has the correct length (32 bytes for 256 bits) + Assert.assertEquals(32, secretKey.encoded.size) + } + + @Test + fun testGenerateKeyFromRawBytes() { + // Create a byte array of 32 bytes (256 bits) filled with a test value + val rawBytes = ByteArray(32) { 0x42.toByte() } + + // Generate a key from the raw bytes + val secretKey = secretKeyGenerator.generateKeyFromRawBytes(rawBytes) + + // Verify that the key is not null + Assert.assertNotNull(secretKey) + + // Verify that the key algorithm is "AES" + Assert.assertEquals("AES", secretKey.algorithm) + + // Verify that the encoded form of the key matches the input raw bytes + Assert.assertArrayEquals(rawBytes, secretKey.encoded) + } + + @Test + fun testGenerateKeyFromRawBytes_VerifyInstance() { + // Create a byte array of 32 bytes (256 bits) + val rawBytes = ByteArray(32) { 0x37.toByte() } + + // Generate a key from the raw bytes + val secretKey = secretKeyGenerator.generateKeyFromRawBytes(rawBytes) + + // Verify that the key is an instance of SecretKeySpec + Assert.assertTrue(secretKey is SecretKeySpec) + } + + @Test + fun testGenerateMultipleRandomKeys_AreUnique() { + // Generate two random keys + val secretKey1 = secretKeyGenerator.generateRandomKey() + val secretKey2 = secretKeyGenerator.generateRandomKey() + + // Verify that the two keys are different + // Note: There is a very small probability that two randomly generated keys could be identical, + // but this is extremely unlikely and would indicate an issue with the random number generator + Assert.assertFalse(secretKey1.encoded.contentEquals(secretKey2.encoded)) + } +}