File: HashedStringCache.java

package info (click to toggle)
android-platform-frameworks-base 1%3A10.0.0%2Br36-3
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 321,788 kB
  • sloc: java: 962,234; cpp: 274,314; xml: 242,770; python: 5,060; sh: 1,432; ansic: 494; makefile: 47; sed: 19
file content (210 lines) | stat: -rw-r--r-- 8,522 bytes parent folder | download | duplicates (4)
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;
        }
    }
}