1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
|
/*
* 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;
}
}
}
|