diff --git a/forgerock-core/src/main/java/org/forgerock/android/auth/AndroidMEncryptor.java b/forgerock-core/src/main/java/org/forgerock/android/auth/AndroidMEncryptor.java deleted file mode 100755 index 647f2add..00000000 --- a/forgerock-core/src/main/java/org/forgerock/android/auth/AndroidMEncryptor.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -package org.forgerock.android.auth; - -import android.security.keystore.KeyGenParameterSpec; -import android.security.keystore.KeyProperties; - -import androidx.annotation.NonNull; - -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; - -/** - * Provide data encryption and decryption for Android M device. - */ -class AndroidMEncryptor extends AbstractSymmetricEncryptor { - - final KeyGenParameterSpec.Builder specBuilder; - - /** - * @param keyAlias The key alias to store the key - */ - AndroidMEncryptor(@NonNull String keyAlias) { - super(keyAlias); - specBuilder = new KeyGenParameterSpec.Builder( - keyAlias, - KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .setRandomizedEncryptionRequired(true) - .setUserAuthenticationRequired(false) - .setKeySize(KEY_SIZE); - } - - @Override - protected SecretKey getSecretKey() throws GeneralSecurityException, IOException { - KeyStore keyStore = getKeyStore(); - if (!keyStore.containsAlias(keyAlias)) { - KeyGenerator keyGenerator = KeyGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE); - keyGenerator.init(specBuilder.build()); - return keyGenerator.generateKey(); - } else { - return ((KeyStore.SecretKeyEntry) keyStore.getEntry(keyAlias, null)).getSecretKey(); - } - } - - @Override - byte[] init(Cipher cipher) throws GeneralSecurityException, IOException { - //Generate a random IV See KeyGenParameterSpec.Builder.setRandomizedEncryptionRequired - cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()); - return cipher.getIV(); - } - - /** - * Retrieve and load the Android KeyStore - * - * @return The AndroidKeyStore - */ - private KeyStore getKeyStore() - throws GeneralSecurityException, IOException { - KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); - keyStore.load(null); - return keyStore; - } - - @Override - public void reset() throws GeneralSecurityException, IOException { - getKeyStore().deleteEntry(keyAlias); - } -} \ No newline at end of file diff --git a/forgerock-core/src/main/java/org/forgerock/android/auth/AndroidMEncryptor.kt b/forgerock-core/src/main/java/org/forgerock/android/auth/AndroidMEncryptor.kt new file mode 100755 index 00000000..fcc79c82 --- /dev/null +++ b/forgerock-core/src/main/java/org/forgerock/android/auth/AndroidMEncryptor.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import android.nfc.Tag +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import java.io.IOException +import java.security.GeneralSecurityException +import java.security.KeyStore +import java.util.concurrent.atomic.AtomicReference +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey + +/** + * Provide data encryption and decryption for Android M device. + */ +internal open class AndroidMEncryptor(keyAlias: String) : AbstractSymmetricEncryptor(keyAlias) { + @JvmField + val specBuilder: KeyGenParameterSpec.Builder = KeyGenParameterSpec.Builder( + keyAlias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setRandomizedEncryptionRequired(true) + .setUserAuthenticationRequired(false) + .setKeySize(KEY_SIZE) + + @Throws(GeneralSecurityException::class, IOException::class) + override fun getSecretKey(): SecretKey { + keyReferenceCache.get()?.let { + Logger.debug(tag, "Secret Key retrieved from cache") + return it + } + if (keyStore.containsAlias(keyAlias)) { + return (keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey.also { + Logger.debug(tag, "Secret Key retrieved from KeyStore and stored in cache") + keyReferenceCache.set(it) + } + } else { + val keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE + ) + keyGenerator.init(specBuilder.build()) + return keyGenerator.generateKey().also { + Logger.debug(tag, "Secret Key generated and stored in cache") + keyReferenceCache.set(it) + } + } + } + + @Throws(GeneralSecurityException::class, IOException::class) + public override fun init(cipher: Cipher): ByteArray { + //Generate a random IV See KeyGenParameterSpec.Builder.setRandomizedEncryptionRequired + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + return cipher.iv + } + + @Throws(GeneralSecurityException::class, IOException::class) + override fun reset() { + keyStore.deleteEntry(keyAlias) + keyReferenceCache.set(null) + Logger.debug(tag, "Secret Key removed from KeyStore and cache") + } + + companion object { + val tag: String = AndroidMEncryptor::class.java.simpleName + //Hold the current key. + val keyReferenceCache = AtomicReference() + + /** + * Retrieve and load the Android KeyStore + * + * @return The AndroidKeyStore + */ + @get:Throws(GeneralSecurityException::class, IOException::class) + private val keyStore: KeyStore + get() { + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) + keyStore.load(null) + return keyStore + } + } +} \ No newline at end of file diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SecuredSharedPreferencesTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SecuredSharedPreferencesTest.java index cc857978..1615c982 100644 --- a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SecuredSharedPreferencesTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SecuredSharedPreferencesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,10 +7,16 @@ package org.forgerock.android.auth; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + import android.content.Context; import android.content.SharedPreferences; + import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; + import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; @@ -22,8 +28,6 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; -import static org.junit.Assert.*; - /** * * Instrumented test, which will execute on an Android device. @@ -55,24 +59,37 @@ public static void tearDown() throws Exception { String filePath = context.getFilesDir().getParent() + "/shared_prefs/test.xml"; File deletePrefFile = new File(filePath); deletePrefFile.delete(); + AndroidMEncryptor.Companion.getKeyReferenceCache().set(null); + } + + @Test + public void testCache() { + AndroidMEncryptor.Companion.getKeyReferenceCache().set(null); + sharedPreferences.edit().putString("Test", "Value").commit(); + assertEquals("Value", sharedPreferences.getString("Test", null)); + assertNotNull(AndroidMEncryptor.Companion.getKeyReferenceCache().get()); } + @Test public void testPutString() { sharedPreferences.edit().putString("Test", "Value").commit(); assertEquals("Value", sharedPreferences.getString("Test", null)); + assertNotNull(AndroidMEncryptor.Companion.getKeyReferenceCache().get()); } @Test public void testPutInt() { sharedPreferences.edit().putInt("Test", 100).commit(); assertEquals(100, sharedPreferences.getInt("Test", 0)); + assertNotNull(AndroidMEncryptor.Companion.getKeyReferenceCache().get()); } @Test public void testPutFloat() { sharedPreferences.edit().putFloat("Test", 1.5f).commit(); assertEquals(1.5f, sharedPreferences.getFloat("Test", 1.5f), 0); + assertNotNull(AndroidMEncryptor.Companion.getKeyReferenceCache().get()); } @Test