blob: 1f2b95650288685e35291de5ebd2874ab7593b93 [file] [log] [blame]
Susi Kharraz-Post14cbfcd2019-04-01 11:07:59 -04001/*
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
17package android.util;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.os.Environment;
22import android.os.storage.StorageManager;
23import android.text.TextUtils;
24
Susi Kharraz-Posta7f61452019-04-04 11:19:20 -040025import com.android.internal.annotations.VisibleForTesting;
26
Susi Kharraz-Post14cbfcd2019-04-01 11:07:59 -040027import java.io.File;
28import java.nio.charset.Charset;
29import java.security.MessageDigest;
30import java.security.NoSuchAlgorithmException;
31import 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.
Susi Kharraz-Post14cbfcd2019-04-01 11:07:59 -040037 * @hide
38 */
39public 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;
Susi Kharraz-Posta7f61452019-04-04 11:19:20 -040044 @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";
Susi Kharraz-Post14cbfcd2019-04-01 11:07:59 -040050 // 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) {
Susi Kharraz-Posta7f61452019-04-04 11:19:20 -0400101 if (saltExpirationDays == -1 || context == null
102 || TextUtils.isEmpty(clearText) || TextUtils.isEmpty(tag)) {
Susi Kharraz-Post14cbfcd2019-04-01 11:07:59 -0400103 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}