content: /*
   * 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 com.android.internal.annotations.VisibleForTesting;
  
  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.
   * @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;
      @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;
      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 (saltExpirationDays == -1 || context == null
                  || TextUtils.isEmpty(clearText) || TextUtils.isEmpty(tag)) {
              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;
          }
      }
  }
  