Loading core/java/android/util/HashedStringCache.java +10 −5 Original line number Diff line number Diff line Loading @@ -22,6 +22,8 @@ import android.os.Environment; import android.os.storage.StorageManager; import android.text.TextUtils; import com.android.internal.annotations.VisibleForTesting; import java.io.File; import java.nio.charset.Charset; import java.security.MessageDigest; Loading @@ -32,7 +34,6 @@ import java.security.SecureRandom; * HashedStringCache provides hashing functionality with an underlying LRUCache and expiring salt. * Salt and expiration time are being stored under the tag passed in by the calling package -- * intended usage is the calling package name. * TODO: Add unit tests b/129870147 * @hide */ public class HashedStringCache { Loading @@ -40,9 +41,12 @@ public class HashedStringCache { private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final int HASH_CACHE_SIZE = 100; private static final int HASH_LENGTH = 8; private static final String HASH_SALT = "_hash_salt"; private static final String HASH_SALT_DATE = "_hash_salt_date"; private static final String HASH_SALT_GEN = "_hash_salt_gen"; @VisibleForTesting static final String HASH_SALT = "_hash_salt"; @VisibleForTesting static final String HASH_SALT_DATE = "_hash_salt_date"; @VisibleForTesting static final String HASH_SALT_GEN = "_hash_salt_gen"; // For privacy we need to rotate the salt regularly private static final long DAYS_TO_MILLIS = 1000 * 60 * 60 * 24; private static final int MAX_SALT_DAYS = 100; Loading Loading @@ -94,7 +98,8 @@ public class HashedStringCache { */ public HashResult hashString(Context context, String tag, String clearText, int saltExpirationDays) { if (TextUtils.isEmpty(clearText) || saltExpirationDays == -1) { if (saltExpirationDays == -1 || context == null || TextUtils.isEmpty(clearText) || TextUtils.isEmpty(tag)) { return null; } Loading core/tests/coretests/src/android/util/HashedStringCacheTest.java 0 → 100644 +189 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.util; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import android.content.Context; import android.content.SharedPreferences; import android.os.Environment; import android.os.storage.StorageManager; import androidx.test.InstrumentationRegistry; import org.junit.Before; import org.junit.Test; import java.io.File; /** * Unit tests for {@link HashedStringCache}. */ public class HashedStringCacheTest { private static final String TAG = "HashedStringCacheTest"; private Context mContext; private static final String TEST_STRING = "test_string"; @Before public void setup() { mContext = null; mContext = InstrumentationRegistry.getContext(); clearSharedPreferences(); } @Test public void testInstanceNotNull() { HashedStringCache cache = HashedStringCache.getInstance(); assertThat(cache, is(notNullValue())); } @Test public void testInstanceMatchesOnSecondCall() { HashedStringCache cache = HashedStringCache.getInstance(); assertThat(HashedStringCache.getInstance(), is(cache)); } @Test public void testHashedStringNotOriginalString() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); assertThat(cachedResult.hashedString, is(not(TEST_STRING))); } @Test public void testThatMultipleCallsResultInSameHash() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); HashedStringCache.HashResult cachedResult2 = cache.hashString(mContext, TAG, TEST_STRING, 7); assertThat(cachedResult2.hashedString, is(cachedResult.hashedString)); } @Test public void testThatZeroDaysResultsInNewHash() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); HashedStringCache.HashResult cachedResult2 = cache.hashString(mContext, TAG, TEST_STRING, 0); assertThat(cachedResult2.hashedString, is(not(cachedResult.hashedString))); } @Test public void testThatNegativeDaysResultsInNewHash() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); HashedStringCache.HashResult cachedResult2 = cache.hashString(mContext, TAG, TEST_STRING, -10); assertThat(cachedResult2.hashedString, is(not(cachedResult.hashedString))); } @Test public void testThatDaysGreater365ResultsInSameResult() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); HashedStringCache.HashResult cachedResult2 = cache.hashString(mContext, TAG, TEST_STRING, 400); assertThat(cachedResult2.hashedString, is(cachedResult.hashedString)); } /** * -1 is treated as a special input to short-circuit out of doing the hashing to give us * the option to turn this feature off if need be while incurring as little computational cost * as possible. */ @Test public void testMinusOneResultsInNull() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, -1); assertThat(cachedResult, is(nullValue())); } @Test public void testEmptyStringInput() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, "", -1); assertThat(cachedResult, is(nullValue())); } @Test public void testNullInput() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, null, -1); assertThat(cachedResult, is(nullValue())); } @Test public void testEmptyStringTag() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, "", TEST_STRING, -1); assertThat(cachedResult, is(nullValue())); } @Test public void testNullTag() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, null, TEST_STRING, -1); assertThat(cachedResult, is(nullValue())); } @Test public void testNullContext() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(null, TAG, TEST_STRING, -1); assertThat(cachedResult, is(nullValue())); } private void clearSharedPreferences() { SharedPreferences preferences = getTestSharedPreferences(mContext); preferences.edit() .remove(TAG + HashedStringCache.HASH_SALT) .remove(TAG + HashedStringCache.HASH_SALT_DATE) .remove(TAG + HashedStringCache.HASH_SALT_GEN).apply(); } /** * Android:ui doesn't have persistent preferences, so need to fall back on this hack originally * from ChooserActivity.java * @param context * @return */ private SharedPreferences getTestSharedPreferences(Context context) { final File prefsFile = new File(new File( Environment.getDataUserCePackageDirectory( StorageManager.UUID_PRIVATE_INTERNAL, context.getUserId(), context.getPackageName()), "shared_prefs"), "hashed_cache_test.xml"); return context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE); } } Loading
core/java/android/util/HashedStringCache.java +10 −5 Original line number Diff line number Diff line Loading @@ -22,6 +22,8 @@ import android.os.Environment; import android.os.storage.StorageManager; import android.text.TextUtils; import com.android.internal.annotations.VisibleForTesting; import java.io.File; import java.nio.charset.Charset; import java.security.MessageDigest; Loading @@ -32,7 +34,6 @@ import java.security.SecureRandom; * HashedStringCache provides hashing functionality with an underlying LRUCache and expiring salt. * Salt and expiration time are being stored under the tag passed in by the calling package -- * intended usage is the calling package name. * TODO: Add unit tests b/129870147 * @hide */ public class HashedStringCache { Loading @@ -40,9 +41,12 @@ public class HashedStringCache { private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final int HASH_CACHE_SIZE = 100; private static final int HASH_LENGTH = 8; private static final String HASH_SALT = "_hash_salt"; private static final String HASH_SALT_DATE = "_hash_salt_date"; private static final String HASH_SALT_GEN = "_hash_salt_gen"; @VisibleForTesting static final String HASH_SALT = "_hash_salt"; @VisibleForTesting static final String HASH_SALT_DATE = "_hash_salt_date"; @VisibleForTesting static final String HASH_SALT_GEN = "_hash_salt_gen"; // For privacy we need to rotate the salt regularly private static final long DAYS_TO_MILLIS = 1000 * 60 * 60 * 24; private static final int MAX_SALT_DAYS = 100; Loading Loading @@ -94,7 +98,8 @@ public class HashedStringCache { */ public HashResult hashString(Context context, String tag, String clearText, int saltExpirationDays) { if (TextUtils.isEmpty(clearText) || saltExpirationDays == -1) { if (saltExpirationDays == -1 || context == null || TextUtils.isEmpty(clearText) || TextUtils.isEmpty(tag)) { return null; } Loading
core/tests/coretests/src/android/util/HashedStringCacheTest.java 0 → 100644 +189 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.util; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import android.content.Context; import android.content.SharedPreferences; import android.os.Environment; import android.os.storage.StorageManager; import androidx.test.InstrumentationRegistry; import org.junit.Before; import org.junit.Test; import java.io.File; /** * Unit tests for {@link HashedStringCache}. */ public class HashedStringCacheTest { private static final String TAG = "HashedStringCacheTest"; private Context mContext; private static final String TEST_STRING = "test_string"; @Before public void setup() { mContext = null; mContext = InstrumentationRegistry.getContext(); clearSharedPreferences(); } @Test public void testInstanceNotNull() { HashedStringCache cache = HashedStringCache.getInstance(); assertThat(cache, is(notNullValue())); } @Test public void testInstanceMatchesOnSecondCall() { HashedStringCache cache = HashedStringCache.getInstance(); assertThat(HashedStringCache.getInstance(), is(cache)); } @Test public void testHashedStringNotOriginalString() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); assertThat(cachedResult.hashedString, is(not(TEST_STRING))); } @Test public void testThatMultipleCallsResultInSameHash() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); HashedStringCache.HashResult cachedResult2 = cache.hashString(mContext, TAG, TEST_STRING, 7); assertThat(cachedResult2.hashedString, is(cachedResult.hashedString)); } @Test public void testThatZeroDaysResultsInNewHash() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); HashedStringCache.HashResult cachedResult2 = cache.hashString(mContext, TAG, TEST_STRING, 0); assertThat(cachedResult2.hashedString, is(not(cachedResult.hashedString))); } @Test public void testThatNegativeDaysResultsInNewHash() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); HashedStringCache.HashResult cachedResult2 = cache.hashString(mContext, TAG, TEST_STRING, -10); assertThat(cachedResult2.hashedString, is(not(cachedResult.hashedString))); } @Test public void testThatDaysGreater365ResultsInSameResult() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, 7); HashedStringCache.HashResult cachedResult2 = cache.hashString(mContext, TAG, TEST_STRING, 400); assertThat(cachedResult2.hashedString, is(cachedResult.hashedString)); } /** * -1 is treated as a special input to short-circuit out of doing the hashing to give us * the option to turn this feature off if need be while incurring as little computational cost * as possible. */ @Test public void testMinusOneResultsInNull() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, TEST_STRING, -1); assertThat(cachedResult, is(nullValue())); } @Test public void testEmptyStringInput() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, "", -1); assertThat(cachedResult, is(nullValue())); } @Test public void testNullInput() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, TAG, null, -1); assertThat(cachedResult, is(nullValue())); } @Test public void testEmptyStringTag() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, "", TEST_STRING, -1); assertThat(cachedResult, is(nullValue())); } @Test public void testNullTag() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(mContext, null, TEST_STRING, -1); assertThat(cachedResult, is(nullValue())); } @Test public void testNullContext() { HashedStringCache cache = HashedStringCache.getInstance(); HashedStringCache.HashResult cachedResult = cache.hashString(null, TAG, TEST_STRING, -1); assertThat(cachedResult, is(nullValue())); } private void clearSharedPreferences() { SharedPreferences preferences = getTestSharedPreferences(mContext); preferences.edit() .remove(TAG + HashedStringCache.HASH_SALT) .remove(TAG + HashedStringCache.HASH_SALT_DATE) .remove(TAG + HashedStringCache.HASH_SALT_GEN).apply(); } /** * Android:ui doesn't have persistent preferences, so need to fall back on this hack originally * from ChooserActivity.java * @param context * @return */ private SharedPreferences getTestSharedPreferences(Context context) { final File prefsFile = new File(new File( Environment.getDataUserCePackageDirectory( StorageManager.UUID_PRIVATE_INTERNAL, context.getUserId(), context.getPackageName()), "shared_prefs"), "hashed_cache_test.xml"); return context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE); } }