summaryrefslogtreecommitdiff
path: root/core/java/android/util/HashedStringCache.java
diff options
context:
space:
mode:
authorSusi Kharraz-Post <susikp@google.com>2019-04-01 11:07:59 -0400
committerSusi Kharraz-Post <susikp@google.com>2019-04-03 17:54:25 -0400
commit14cbfcdbd0430fa0e2fbe9083795dc6a2764db3f (patch)
tree60d04f8d8f7566def7afb568995836fe4e163672 /core/java/android/util/HashedStringCache.java
parent92aa9b2fbaa60710f1b0ccf44770eef1d5ab5b2a (diff)
Add logging for direct share target
To answer the question if users share mainly with 1 or 2 direct targets or with a multitude of contacts, we need to log the direct target + package name. For privacy, this gets hashed with a salt that expires by default every 7 days. The PH flag will allow us to change the expiration time if we obtain PWG permission for that. Bug: 126365511 Test: New test in ChooserActivityTest + manual testing of consistency and flag rollout using adb shell device_config put systemui hash_salt_max_days with multiple values Change-Id: Ib4255b3eb39ca91ccb5803dc036ffe0ea83a27c9
Diffstat (limited to 'core/java/android/util/HashedStringCache.java')
-rw-r--r--core/java/android/util/HashedStringCache.java205
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;
+ }
+ }
+}