Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2019 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.util;
     18 
     19 import android.content.Context;
     20 import android.content.SharedPreferences;
     21 import android.os.Environment;
     22 import android.os.storage.StorageManager;
     23 import android.text.TextUtils;
     24 
     25 import com.android.internal.annotations.VisibleForTesting;
     26 
     27 import java.io.File;
     28 import java.nio.charset.Charset;
     29 import java.security.MessageDigest;
     30 import java.security.NoSuchAlgorithmException;
     31 import java.security.SecureRandom;
     32 
     33 /**
     34  * HashedStringCache provides hashing functionality with an underlying LRUCache and expiring salt.
     35  * Salt and expiration time are being stored under the tag passed in by the calling package --
     36  * intended usage is the calling package name.
     37  * @hide
     38  */
     39 public class HashedStringCache {
     40     private static HashedStringCache sHashedStringCache = null;
     41     private static final Charset UTF_8 = Charset.forName("UTF-8");
     42     private static final int HASH_CACHE_SIZE = 100;
     43     private static final int HASH_LENGTH = 8;
     44     @VisibleForTesting
     45     static final String HASH_SALT = "_hash_salt";
     46     @VisibleForTesting
     47     static final String HASH_SALT_DATE = "_hash_salt_date";
     48     @VisibleForTesting
     49     static final String HASH_SALT_GEN = "_hash_salt_gen";
     50     // For privacy we need to rotate the salt regularly
     51     private static final long DAYS_TO_MILLIS = 1000 * 60 * 60 * 24;
     52     private static final int MAX_SALT_DAYS = 100;
     53     private final LruCache<String, String> mHashes;
     54     private final SecureRandom mSecureRandom;
     55     private final Object mPreferenceLock = new Object();
     56     private final MessageDigest mDigester;
     57     private byte[] mSalt;
     58     private int mSaltGen;
     59     private SharedPreferences mSharedPreferences;
     60 
     61     private static final String TAG = "HashedStringCache";
     62     private static final boolean DEBUG = false;
     63 
     64     private HashedStringCache() {
     65         mHashes = new LruCache<>(HASH_CACHE_SIZE);
     66         mSecureRandom = new SecureRandom();
     67         try {
     68             mDigester = MessageDigest.getInstance("MD5");
     69         } catch (NoSuchAlgorithmException impossible) {
     70             // this can't happen - MD5 is always present
     71             throw new RuntimeException(impossible);
     72         }
     73     }
     74 
     75     /**
     76      * @return - instance of the HashedStringCache
     77      * @hide
     78      */
     79     public static HashedStringCache getInstance() {
     80         if (sHashedStringCache == null) {
     81             sHashedStringCache = new HashedStringCache();
     82         }
     83         return sHashedStringCache;
     84     }
     85 
     86     /**
     87      * Take the string and context and create a hash of the string. Trigger refresh on salt if salt
     88      * is more than 7 days old
     89      * @param context - callers context to retrieve SharedPreferences
     90      * @param clearText - string that needs to be hashed
     91      * @param tag - class name to use for storing values in shared preferences
     92      * @param saltExpirationDays - number of days we may keep the same salt
     93      *                           special value -1 will short-circuit and always return null.
     94      * @return - HashResult containing the hashed string and the generation of the hash salt, null
     95      *      if clearText string is empty
     96      *
     97      * @hide
     98      */
     99     public HashResult hashString(Context context, String tag, String clearText,
    100             int saltExpirationDays) {
    101         if (saltExpirationDays == -1 || context == null
    102                 || TextUtils.isEmpty(clearText) || TextUtils.isEmpty(tag)) {
    103             return null;
    104         }
    105 
    106         populateSaltValues(context, tag, saltExpirationDays);
    107         String hashText = mHashes.get(clearText);
    108         if (hashText != null) {
    109             return new HashResult(hashText, mSaltGen);
    110         }
    111 
    112         mDigester.reset();
    113         mDigester.update(mSalt);
    114         mDigester.update(clearText.getBytes(UTF_8));
    115         byte[] bytes = mDigester.digest();
    116         int len = Math.min(HASH_LENGTH, bytes.length);
    117         hashText = Base64.encodeToString(bytes, 0, len, Base64.NO_PADDING | Base64.NO_WRAP);
    118         mHashes.put(clearText, hashText);
    119 
    120         return new HashResult(hashText, mSaltGen);
    121     }
    122 
    123     /**
    124      * Populates the mSharedPreferences and checks if there is a salt present and if it's older than
    125      * 7 days
    126      * @param tag - class name to use for storing values in shared preferences
    127      * @param saltExpirationDays - number of days we may keep the same salt
    128      * @param saltDate - the date retrieved from configuration
    129      * @return - true if no salt or salt is older than 7 days
    130      */
    131     private boolean checkNeedsNewSalt(String tag, int saltExpirationDays, long saltDate) {
    132         if (saltDate == 0 || saltExpirationDays < -1) {
    133             return true;
    134         }
    135         if (saltExpirationDays > MAX_SALT_DAYS) {
    136             saltExpirationDays = MAX_SALT_DAYS;
    137         }
    138         long now = System.currentTimeMillis();
    139         long delta = now - saltDate;
    140         // Check for delta < 0 to make sure we catch if someone puts their phone far in the
    141         // future and then goes back to normal time.
    142         return delta >= saltExpirationDays * DAYS_TO_MILLIS || delta < 0;
    143     }
    144 
    145     /**
    146      * Populate the salt and saltGen member variables if they aren't already set / need refreshing.
    147      * @param context - to get sharedPreferences
    148      * @param tag - class name to use for storing values in shared preferences
    149      * @param saltExpirationDays - number of days we may keep the same salt
    150      */
    151     private void populateSaltValues(Context context, String tag, int saltExpirationDays) {
    152         synchronized (mPreferenceLock) {
    153             // check if we need to refresh the salt
    154             mSharedPreferences = getHashSharedPreferences(context);
    155             long saltDate = mSharedPreferences.getLong(tag + HASH_SALT_DATE, 0);
    156             boolean needsNewSalt = checkNeedsNewSalt(tag, saltExpirationDays, saltDate);
    157             if (needsNewSalt) {
    158                 mHashes.evictAll();
    159             }
    160             if (mSalt == null || needsNewSalt) {
    161                 String saltString = mSharedPreferences.getString(tag + HASH_SALT, null);
    162                 mSaltGen = mSharedPreferences.getInt(tag + HASH_SALT_GEN, 0);
    163                 if (saltString == null || needsNewSalt) {
    164                     mSaltGen++;
    165                     byte[] saltBytes = new byte[16];
    166                     mSecureRandom.nextBytes(saltBytes);
    167                     saltString = Base64.encodeToString(saltBytes,
    168                             Base64.NO_PADDING | Base64.NO_WRAP);
    169                     mSharedPreferences.edit()
    170                             .putString(tag + HASH_SALT, saltString)
    171                             .putInt(tag + HASH_SALT_GEN, mSaltGen)
    172                             .putLong(tag + HASH_SALT_DATE, System.currentTimeMillis()).apply();
    173                     if (DEBUG) {
    174                         Log.d(TAG, "created a new salt: " + saltString);
    175                     }
    176                 }
    177                 mSalt = saltString.getBytes(UTF_8);
    178             }
    179         }
    180     }
    181 
    182     /**
    183      * Android:ui doesn't have persistent preferences, so need to fall back on this hack originally
    184      * from ChooserActivity.java
    185      * @param context
    186      * @return
    187      */
    188     private SharedPreferences getHashSharedPreferences(Context context) {
    189         final File prefsFile = new File(new File(
    190                 Environment.getDataUserCePackageDirectory(
    191                         StorageManager.UUID_PRIVATE_INTERNAL,
    192                         context.getUserId(), context.getPackageName()),
    193                 "shared_prefs"),
    194                 "hashed_cache.xml");
    195         return context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE);
    196     }
    197 
    198     /**
    199      * Helper class to hold hashed string and salt generation.
    200      */
    201     public class HashResult {
    202         public String hashedString;
    203         public int saltGeneration;
    204 
    205         public HashResult(String hString, int saltGen) {
    206             hashedString = hString;
    207             saltGeneration = saltGen;
    208         }
    209     }
    210 }
    211