From ab924d35d32d2be746c49f2e22b6859c92092744 Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 30 Sep 2020 20:21:18 -0400 Subject: [PATCH 1/5] Minor documentation fixes. --- .../securestoragelibrary/SecurePreferences.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java b/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java index 58b3550..49913ec 100644 --- a/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java +++ b/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java @@ -65,7 +65,7 @@ public static void setValue(@NonNull Context context, } /** - * Takes plain string value, encrypts it and stores it encrypted in the SecureStorage on the Android Device + * Takes plain boolean value, encrypts it and stores it encrypted in the SecureStorage on the Android Device * * @param context Context is used internally * @param key Key used to identify the stored value in SecureStorage @@ -78,7 +78,7 @@ public static void setValue(@NonNull Context context, } /** - * Takes plain string value, encrypts it and stores it encrypted in the SecureStorage on the Android Device + * Takes plain float value, encrypts it and stores it encrypted in the SecureStorage on the Android Device * * @param context Context is used internally * @param key Key used to identify the stored value in SecureStorage @@ -91,7 +91,7 @@ public static void setValue(@NonNull Context context, } /** - * Takes plain string value, encrypts it and stores it encrypted in the SecureStorage on the Android Device + * Takes plain long value, encrypts it and stores it encrypted in the SecureStorage on the Android Device * * @param context Context is used internally * @param key Key used to identify the stored value in SecureStorage @@ -104,7 +104,7 @@ public static void setValue(@NonNull Context context, } /** - * Takes plain string value, encrypts it and stores it encrypted in the SecureStorage on the Android Device + * Takes plain int value, encrypts it and stores it encrypted in the SecureStorage on the Android Device * * @param context Context is used internally * @param key Key used to identify the stored value in SecureStorage @@ -117,11 +117,11 @@ public static void setValue(@NonNull Context context, } /** - * Takes plain string value, encrypts it and stores it encrypted in the SecureStorage on the Android Device + * Takes plain Set<String> value, encrypts it and stores it encrypted in the SecureStorage on the Android Device * * @param context Context is used internally * @param key Key used to identify the stored value in SecureStorage - * @param value Plain Set(type: String) value that will be encrypted and stored in the SecureStorage + * @param value Plain Set<String> value that will be encrypted and stored in the SecureStorage */ public static void setValue(@NonNull Context context, @NonNull String key, From 137401a2b3853119f27febe5bc739986ee1d78e7 Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 30 Sep 2020 20:32:19 -0400 Subject: [PATCH 2/5] Add contains logic for collection keys. Closes https://github.com/adorsys/secure-storage-android/issues/89 --- .../SecurePreferences.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java b/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java index 49913ec..b36aaa0 100644 --- a/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java +++ b/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java @@ -254,7 +254,8 @@ public static boolean contains(@NonNull Context context, SharedPreferences preferences = applicationContext .getSharedPreferences(KEY_SHARED_PREFERENCES_NAME, MODE_PRIVATE); try { - return preferences.contains(key) && KeystoreTool.keyPairExists(); + return (preferences.contains(key) || containsCollection(context, key)) + && KeystoreTool.keyPairExists(); } catch (SecureStorageException e) { return false; } @@ -337,6 +338,34 @@ private static void removeSecureValue(@NonNull Context context, preferences.edit().remove(key).apply(); } + + /** + * Checks if SecureStorage contains a String Set value for the given key. Since sets are stored + * under multiple keys, this verifies that each expected key is present but does not verify the + * value types of those keys. + * + * @param context Context is used internally + * @param key Key used to identify the stored value in SecureStorage + * @return True if the keys for the set value exist in SecureStorage, otherwise false + */ + private static boolean containsCollection(@NonNull Context context, + @NonNull String key) { + Context applicationContext = context.getApplicationContext(); + SharedPreferences preferences = applicationContext + .getSharedPreferences(KEY_SHARED_PREFERENCES_NAME, MODE_PRIVATE); + + int size = getIntValue(context, key + KEY_SET_COUNT_POSTFIX, -1); + + if (size == -1) { + return false; + } + + for (int i = 0; i < size; i++) { + if (!preferences.contains(key + "_" + i)) return false; + } + return true; + } + private static void clearAllSecureValues(@NonNull Context context) { SharedPreferences preferences = context .getSharedPreferences(KEY_SHARED_PREFERENCES_NAME, MODE_PRIVATE); From 731a70d7a2c1b3721b81da1b7c44fe072ef355dd Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 30 Sep 2020 20:37:07 -0400 Subject: [PATCH 3/5] Fixes removeValue logic for collection keys. Closes https://github.com/adorsys/secure-storage-android/issues/90 --- .../securestoragelibrary/SecurePreferences.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java b/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java index b36aaa0..6ca984a 100644 --- a/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java +++ b/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java @@ -271,6 +271,7 @@ public static void removeValue(@NonNull Context context, @NonNull String key) { Context applicationContext = context.getApplicationContext(); removeSecureValue(applicationContext, key); + removeSecureCollectionValue(applicationContext, key); } /** @@ -338,6 +339,17 @@ private static void removeSecureValue(@NonNull Context context, preferences.edit().remove(key).apply(); } + private static void removeSecureCollectionValue(@NonNull Context context, + @NonNull String key) { + int size = getIntValue(context, key + KEY_SET_COUNT_POSTFIX, -1); + + if (size != -1) { + for (int i = 0; i < size; i++) { + removeSecureValue(context, key + "_" + i); + } + removeSecureValue(context,key + KEY_SET_COUNT_POSTFIX); + } + } /** * Checks if SecureStorage contains a String Set value for the given key. Since sets are stored From 6d2abde8e364014140833c1d153e7bd7ac1d4c42 Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 30 Sep 2020 20:39:37 -0400 Subject: [PATCH 4/5] Add ability to store lists. This better serves the usecases of storing ordered strings since Lists are expected to be ordered. Also adjusts the Set behavior to be compatible with ordered sets. Closes https://github.com/adorsys/secure-storage-android/issues/91 --- .../SecurePreferences.java | 101 ++++++++++++++---- 1 file changed, 80 insertions(+), 21 deletions(-) diff --git a/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java b/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java index 6ca984a..0401c75 100644 --- a/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java +++ b/securestoragelibrary/src/main/java/de/adorsys/android/securestoragelibrary/SecurePreferences.java @@ -20,12 +20,15 @@ import android.content.SharedPreferences; import android.text.TextUtils; -import java.util.HashSet; -import java.util.Set; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + import static android.content.Context.MODE_PRIVATE; import static de.adorsys.android.securestoragelibrary.SecureStorageException.ExceptionType.CRYPTO_EXCEPTION; @@ -126,12 +129,20 @@ public static void setValue(@NonNull Context context, public static void setValue(@NonNull Context context, @NonNull String key, @NonNull Set value) throws SecureStorageException { - setValue(context, key + KEY_SET_COUNT_POSTFIX, String.valueOf(value.size())); + setCollectionValue(context, key, value); + } - int i = 0; - for (String s : value) { - setValue(context, key + "_" + (i++), s); - } + /** + * Takes plain List<String> value, encrypts it and stores it encrypted in the SecureStorage on the Android Device + * + * @param context Context is used internally + * @param key Key used to identify the stored value in SecureStorage + * @param value Plain List<String> value that will be encrypted and stored in the SecureStorage + */ + public static void setValue(@NonNull Context context, + @NonNull String key, + @NonNull List value) throws SecureStorageException { + setCollectionValue(context, key, value); } /** @@ -216,29 +227,33 @@ public static int getIntValue(@NonNull Context context, } /** - * Gets encrypted int value for given key from the SecureStorage on the Android Device, decrypts it and returns it + * Gets encrypted Set<String> value for given key from the SecureStorage on the Android Device, decrypts it and returns it * * @param context Context is used internally * @param key Key used to identify the stored value in SecureStorage * @param defValue Default Set(type: String) value that will be returned if the value with given key doesn't exist or an exception is thrown - * @return Decrypted Set(type: String) value associated with given key from SecureStorage + * @return Decrypted Set<String> value associated with given key from SecureStorage */ @NonNull public static Set getStringSetValue(@NonNull Context context, @NonNull String key, @NonNull Set defValue) { - int size = getIntValue(context, key + KEY_SET_COUNT_POSTFIX, -1); - - if (size == -1) { - return defValue; - } - - Set res = new HashSet<>(size); - for (int i = 0; i < size; i++) { - res.add(getStringValue(context, key + "_" + i, "")); - } + return getStringCollectionValue(context, key, defValue, new LinkedHashSet<>()); + } - return res; + /** + * Gets encrypted List<String> value for given key from the SecureStorage on the Android Device, decrypts it and returns it + * + * @param context Context is used internally + * @param key Key used to identify the stored value in SecureStorage + * @param defValue Default List<String> value that will be returned if the value with given key doesn't exist or an exception is thrown + * @return Decrypted List<String> value associated with given key from SecureStorage + */ + @NonNull + public static List getStringListValue(@NonNull Context context, + @NonNull String key, + @NonNull List defValue) { + return getStringCollectionValue(context, key, defValue, new ArrayList<>()); } /** @@ -324,6 +339,24 @@ private static void setSecureValue(@NonNull Context context, preferences.edit().putString(key, value).apply(); } + /** + * Takes plain Collection<String> value, encrypts it and stores it encrypted in the SecureStorage on the Android Device + * + * @param context Context is used internally + * @param key Key used to identify the stored value in SecureStorage + * @param value Plain Collection<String> value that will be encrypted and stored in the SecureStorage + */ + private static void setCollectionValue(@NonNull Context context, + @NonNull String key, + @NonNull Collection value) throws SecureStorageException { + setValue(context, key + KEY_SET_COUNT_POSTFIX, String.valueOf(value.size())); + + int i = 0; + for (String s : value) { + setValue(context, key + "_" + (i++), s); + } + } + @Nullable private static String getSecureValue(@NonNull Context context, @NonNull String key) { @@ -332,6 +365,32 @@ private static String getSecureValue(@NonNull Context context, return preferences.getString(key, null); } + /** + * Gets encrypted Collection<String> value for given key from the SecureStorage on the Android Device, decrypts it and returns it + * + * @param context Context is used internally + * @param key Key used to identify the stored value in SecureStorage + * @param defValue Default Set(type: String) value that will be returned if the value with given key doesn't exist or an exception is thrown + * @return Decrypted Collection<String> value associated with given key from SecureStorage + */ + @NonNull + private static > T getStringCollectionValue(@NonNull Context context, + @NonNull String key, + @NonNull T defValue, + @NonNull T retValue) { + int size = getIntValue(context, key + KEY_SET_COUNT_POSTFIX, -1); + + if (size == -1) { + return defValue; + } + + for (int i = 0; i < size; i++) { + retValue.add(getStringValue(context, key + "_" + i, "")); + } + + return retValue; + } + private static void removeSecureValue(@NonNull Context context, @NonNull String key) { SharedPreferences preferences = context From 782345e100b80cdb42756f6f4a3c04cfa3fc3d14 Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 30 Sep 2020 20:40:34 -0400 Subject: [PATCH 5/5] Add tests to validate new ordered list behavior. --- .../SecureStorageLogicTest.kt | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/app/src/androidTest/java/de/adorsys/android/securestoragetest/SecureStorageLogicTest.kt b/app/src/androidTest/java/de/adorsys/android/securestoragetest/SecureStorageLogicTest.kt index 67c1f17..8c8804b 100644 --- a/app/src/androidTest/java/de/adorsys/android/securestoragetest/SecureStorageLogicTest.kt +++ b/app/src/androidTest/java/de/adorsys/android/securestoragetest/SecureStorageLogicTest.kt @@ -51,6 +51,76 @@ open class SecureStorageLogicTest : SecureStorageBaseTest() { SecurePreferences.clearAllValues(context) } + @Test + fun testStoreRetrieveAndRemoveStringSetValue() { + val KEY_STRING_SET = "KEY_STRING_SET" + val VALUE_STRING_SET = setOf( + "The wheels on the \uD83D\uDE8C go, Round and round, Round and round, Round and round.", + "The wheels on the \uD83D\uDE8C go Round and round, All through the town. The doors on", + "the \uD83D\uDE8C go, Open and shut ♫, Open and shut ♫, Open and shut." + ) + val context = activityRule.activity.applicationContext + + // Store a String Set value in SecureStorage + SecurePreferences.setValue(context, KEY_STRING_SET, VALUE_STRING_SET) + + // Check if the value exists in SecureStorage + Assert.assertTrue(SecurePreferences.contains(context, KEY_STRING_SET)) + + // Retrieve the previously stored String Set value from the SecureStorage + val retrievedValue: MutableSet = SecurePreferences.getStringSetValue(context, KEY_STRING_SET, setOf()) + + // Check if the retrievedValue is not null + Assert.assertEquals(VALUE_STRING_SET.size, retrievedValue.size) + + // Check if the retrievedValue equals the pre-stored value + Assert.assertEquals(VALUE_STRING_SET, retrievedValue) + + // Remove the String Set value from SecureStorage + SecurePreferences.removeValue(context, KEY_STRING_SET) + + // Check if the String Set value has been removed from SecureStorage + Assert.assertFalse(SecurePreferences.contains(context, KEY_STRING_SET)) + + // Delete keys and clear SecureStorage + SecurePreferences.clearAllValues(context) + } + + @Test + fun testStoreRetrieveAndRemoveStringListValue() { + val KEY_STRING_LIST = "KEY_STRING_LIST" + val VALUE_STRING_LIST = listOf( + "The wheels on the \uD83D\uDE8C go, Round and round, Round and round, Round and round.", + "The wheels on the \uD83D\uDE8C go Round and round, All through the town. The doors on", + "the \uD83D\uDE8C go, Open and shut ♫, Open and shut ♫, Open and shut." + ) + val context = activityRule.activity.applicationContext + + // Store a String Set value in SecureStorage + SecurePreferences.setValue(context, KEY_STRING_LIST, VALUE_STRING_LIST) + + // Check if the value exists in SecureStorage + Assert.assertTrue(SecurePreferences.contains(context, KEY_STRING_LIST)) + + // Retrieve the previously stored String Set value from the SecureStorage + val retrievedValue = SecurePreferences.getStringListValue(context, KEY_STRING_LIST, listOf()) + + // Check if the retrievedValue is not null + Assert.assertEquals(VALUE_STRING_LIST.size, retrievedValue.size) + + // Check if the retrievedValue equals the pre-stored value + Assert.assertEquals(VALUE_STRING_LIST, retrievedValue) + + // Remove the String Set value from SecureStorage + SecurePreferences.removeValue(context, KEY_STRING_LIST) + + // Check if the String Set value has been removed from SecureStorage + Assert.assertFalse(SecurePreferences.contains(context, KEY_STRING_LIST)) + + // Delete keys and clear SecureStorage + SecurePreferences.clearAllValues(context) + } + @Test fun testStoreRetrieveAndRemoveBooleanValue() { val KEY_BOOLEAN = "KEY_BOOLEAN"