diff options
Diffstat (limited to 'core/java/android/util/HashedStringCache.java')
| -rw-r--r-- | core/java/android/util/HashedStringCache.java | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/core/java/android/util/HashedStringCache.java b/core/java/android/util/HashedStringCache.java new file mode 100644 index 000000000000..8ce85148c7c2 --- /dev/null +++ b/core/java/android/util/HashedStringCache.java @@ -0,0 +1,205 @@ +/* + * 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 android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.os.storage.StorageManager; +import android.text.TextUtils; + +import java.io.File; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +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 { + private static HashedStringCache sHashedStringCache = null; + 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"; + // 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; + private final LruCache<String, String> mHashes; + private final SecureRandom mSecureRandom; + private final Object mPreferenceLock = new Object(); + private final MessageDigest mDigester; + private byte[] mSalt; + private int mSaltGen; + private SharedPreferences mSharedPreferences; + + private static final String TAG = "HashedStringCache"; + private static final boolean DEBUG = false; + + private HashedStringCache() { + mHashes = new LruCache<>(HASH_CACHE_SIZE); + mSecureRandom = new SecureRandom(); + try { + mDigester = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException impossible) { + // this can't happen - MD5 is always present + throw new RuntimeException(impossible); + } + } + + /** + * @return - instance of the HashedStringCache + * @hide + */ + public static HashedStringCache getInstance() { + if (sHashedStringCache == null) { + sHashedStringCache = new HashedStringCache(); + } + return sHashedStringCache; + } + + /** + * Take the string and context and create a hash of the string. Trigger refresh on salt if salt + * is more than 7 days old + * @param context - callers context to retrieve SharedPreferences + * @param clearText - string that needs to be hashed + * @param tag - class name to use for storing values in shared preferences + * @param saltExpirationDays - number of days we may keep the same salt + * special value -1 will short-circuit and always return null. + * @return - HashResult containing the hashed string and the generation of the hash salt, null + * if clearText string is empty + * + * @hide + */ + public HashResult hashString(Context context, String tag, String clearText, + int saltExpirationDays) { + if (TextUtils.isEmpty(clearText) || saltExpirationDays == -1) { + return null; + } + + populateSaltValues(context, tag, saltExpirationDays); + String hashText = mHashes.get(clearText); + if (hashText != null) { + return new HashResult(hashText, mSaltGen); + } + + mDigester.reset(); + mDigester.update(mSalt); + mDigester.update(clearText.getBytes(UTF_8)); + byte[] bytes = mDigester.digest(); + int len = Math.min(HASH_LENGTH, bytes.length); + hashText = Base64.encodeToString(bytes, 0, len, Base64.NO_PADDING | Base64.NO_WRAP); + mHashes.put(clearText, hashText); + + return new HashResult(hashText, mSaltGen); + } + + /** + * Populates the mSharedPreferences and checks if there is a salt present and if it's older than + * 7 days + * @param tag - class name to use for storing values in shared preferences + * @param saltExpirationDays - number of days we may keep the same salt + * @param saltDate - the date retrieved from configuration + * @return - true if no salt or salt is older than 7 days + */ + private boolean checkNeedsNewSalt(String tag, int saltExpirationDays, long saltDate) { + if (saltDate == 0 || saltExpirationDays < -1) { + return true; + } + if (saltExpirationDays > MAX_SALT_DAYS) { + saltExpirationDays = MAX_SALT_DAYS; + } + long now = System.currentTimeMillis(); + long delta = now - saltDate; + // Check for delta < 0 to make sure we catch if someone puts their phone far in the + // future and then goes back to normal time. + return delta >= saltExpirationDays * DAYS_TO_MILLIS || delta < 0; + } + + /** + * Populate the salt and saltGen member variables if they aren't already set / need refreshing. + * @param context - to get sharedPreferences + * @param tag - class name to use for storing values in shared preferences + * @param saltExpirationDays - number of days we may keep the same salt + */ + private void populateSaltValues(Context context, String tag, int saltExpirationDays) { + synchronized (mPreferenceLock) { + // check if we need to refresh the salt + mSharedPreferences = getHashSharedPreferences(context); + long saltDate = mSharedPreferences.getLong(tag + HASH_SALT_DATE, 0); + boolean needsNewSalt = checkNeedsNewSalt(tag, saltExpirationDays, saltDate); + if (needsNewSalt) { + mHashes.evictAll(); + } + if (mSalt == null || needsNewSalt) { + String saltString = mSharedPreferences.getString(tag + HASH_SALT, null); + mSaltGen = mSharedPreferences.getInt(tag + HASH_SALT_GEN, 0); + if (saltString == null || needsNewSalt) { + mSaltGen++; + byte[] saltBytes = new byte[16]; + mSecureRandom.nextBytes(saltBytes); + saltString = Base64.encodeToString(saltBytes, + Base64.NO_PADDING | Base64.NO_WRAP); + mSharedPreferences.edit() + .putString(tag + HASH_SALT, saltString) + .putInt(tag + HASH_SALT_GEN, mSaltGen) + .putLong(tag + HASH_SALT_DATE, System.currentTimeMillis()).apply(); + if (DEBUG) { + Log.d(TAG, "created a new salt: " + saltString); + } + } + mSalt = saltString.getBytes(UTF_8); + } + } + } + + /** + * Android:ui doesn't have persistent preferences, so need to fall back on this hack originally + * from ChooserActivity.java + * @param context + * @return + */ + private SharedPreferences getHashSharedPreferences(Context context) { + final File prefsFile = new File(new File( + Environment.getDataUserCePackageDirectory( + StorageManager.UUID_PRIVATE_INTERNAL, + context.getUserId(), context.getPackageName()), + "shared_prefs"), + "hashed_cache.xml"); + return context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE); + } + + /** + * Helper class to hold hashed string and salt generation. + */ + public class HashResult { + public String hashedString; + public int saltGeneration; + + public HashResult(String hString, int saltGen) { + hashedString = hString; + saltGeneration = saltGen; + } + } +} |
