diff options
-rw-r--r-- | api/current.txt | 1 | ||||
-rw-r--r-- | api/system-current.txt | 1 | ||||
-rw-r--r-- | core/java/android/accounts/AbstractAccountAuthenticator.java | 61 | ||||
-rw-r--r-- | services/core/java/com/android/server/accounts/AccountManagerService.java | 203 | ||||
-rw-r--r-- | services/core/java/com/android/server/accounts/TokenCache.java | 163 |
5 files changed, 398 insertions, 31 deletions
diff --git a/api/current.txt b/api/current.txt index d38e75a..418cbe5 100644 --- a/api/current.txt +++ b/api/current.txt @@ -2716,6 +2716,7 @@ package android.accounts { method public final android.os.IBinder getIBinder(); method public abstract android.os.Bundle hasFeatures(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String[]) throws android.accounts.NetworkErrorException; method public abstract android.os.Bundle updateCredentials(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String, android.os.Bundle) throws android.accounts.NetworkErrorException; + field public static final java.lang.String KEY_CUSTOM_TOKEN_EXPIRY = "android.accounts.expiry"; } public class Account implements android.os.Parcelable { diff --git a/api/system-current.txt b/api/system-current.txt index 44d8c8e..b4ed5e9 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -2797,6 +2797,7 @@ package android.accounts { method public final android.os.IBinder getIBinder(); method public abstract android.os.Bundle hasFeatures(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String[]) throws android.accounts.NetworkErrorException; method public abstract android.os.Bundle updateCredentials(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String, android.os.Bundle) throws android.accounts.NetworkErrorException; + field public static final java.lang.String KEY_CUSTOM_TOKEN_EXPIRY = "android.accounts.expiry"; } public class Account implements android.os.Parcelable { diff --git a/core/java/android/accounts/AbstractAccountAuthenticator.java b/core/java/android/accounts/AbstractAccountAuthenticator.java index dbc9051..3e4a66d 100644 --- a/core/java/android/accounts/AbstractAccountAuthenticator.java +++ b/core/java/android/accounts/AbstractAccountAuthenticator.java @@ -108,6 +108,14 @@ import java.util.Arrays; public abstract class AbstractAccountAuthenticator { private static final String TAG = "AccountAuthenticator"; + /** + * Bundle key used for the {@code long} expiration time (in millis from the unix epoch) of the + * associated auth token. + * + * @see #getAuthToken + */ + public static final String KEY_CUSTOM_TOKEN_EXPIRY = "android.accounts.expiry"; + private final Context mContext; public AbstractAccountAuthenticator(Context context) { @@ -115,6 +123,7 @@ public abstract class AbstractAccountAuthenticator { } private class Transport extends IAccountAuthenticator.Stub { + @Override public void addAccount(IAccountAuthenticatorResponse response, String accountType, String authTokenType, String[] features, Bundle options) throws RemoteException { @@ -140,6 +149,7 @@ public abstract class AbstractAccountAuthenticator { } } + @Override public void confirmCredentials(IAccountAuthenticatorResponse response, Account account, Bundle options) throws RemoteException { if (Log.isLoggable(TAG, Log.VERBOSE)) { @@ -162,6 +172,7 @@ public abstract class AbstractAccountAuthenticator { } } + @Override public void getAuthTokenLabel(IAccountAuthenticatorResponse response, String authTokenType) throws RemoteException { @@ -184,6 +195,7 @@ public abstract class AbstractAccountAuthenticator { } } + @Override public void getAuthToken(IAccountAuthenticatorResponse response, Account account, String authTokenType, Bundle loginOptions) throws RemoteException { @@ -209,6 +221,7 @@ public abstract class AbstractAccountAuthenticator { } } + @Override public void updateCredentials(IAccountAuthenticatorResponse response, Account account, String authTokenType, Bundle loginOptions) throws RemoteException { if (Log.isLoggable(TAG, Log.VERBOSE)) { @@ -234,6 +247,7 @@ public abstract class AbstractAccountAuthenticator { } } + @Override public void editProperties(IAccountAuthenticatorResponse response, String accountType) throws RemoteException { checkBinderPermission(); @@ -248,6 +262,7 @@ public abstract class AbstractAccountAuthenticator { } } + @Override public void hasFeatures(IAccountAuthenticatorResponse response, Account account, String[] features) throws RemoteException { checkBinderPermission(); @@ -262,6 +277,7 @@ public abstract class AbstractAccountAuthenticator { } } + @Override public void getAccountRemovalAllowed(IAccountAuthenticatorResponse response, Account account) throws RemoteException { checkBinderPermission(); @@ -276,6 +292,7 @@ public abstract class AbstractAccountAuthenticator { } } + @Override public void getAccountCredentialsForCloning(IAccountAuthenticatorResponse response, Account account) throws RemoteException { checkBinderPermission(); @@ -291,6 +308,7 @@ public abstract class AbstractAccountAuthenticator { } } + @Override public void addAccountFromCredentials(IAccountAuthenticatorResponse response, Account account, Bundle accountCredentials) throws RemoteException { @@ -410,21 +428,42 @@ public abstract class AbstractAccountAuthenticator { public abstract Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException; + /** - * Gets the authtoken for an account. + * Gets an authtoken for an account. + * + * If not {@code null}, the resultant {@link Bundle} will contain different sets of keys + * depending on whether a token was successfully issued and, if not, whether one + * could be issued via some {@link android.app.Activity}. + * <p> + * If a token cannot be provided without some additional activity, the Bundle should contain + * {@link AccountManager#KEY_INTENT} with an associated {@link Intent}. On the other hand, if + * there is no such activity, then a Bundle containing + * {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} should be + * returned. + * <p> + * If a token can be successfully issued, the implementation should return the + * {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of the + * account associated with the token as well as the {@link AccountManager#KEY_AUTHTOKEN}. In + * addition {@link AbstractAccountAuthenticator} implementations that declare themselves + * {@code android:customTokens=true} may also provide a non-negative {@link + * #KEY_CUSTOM_TOKEN_EXPIRY} long value containing the expiration timestamp of the expiration + * time (in millis since the unix epoch). + * <p> + * Implementers should assume that tokens will be cached on the basis of account and + * authTokenType. The system may ignore the contents of the supplied options Bundle when + * determining to re-use a cached token. Furthermore, implementers should assume a supplied + * expiration time will be treated as non-binding advice. + * <p> + * Finally, note that for android:customTokens=false authenticators, tokens are cached + * indefinitely until some client calls {@link + * AccountManager#invalidateAuthToken(String,String)}. + * * @param response to send the result back to the AccountManager, will never be null * @param account the account whose credentials are to be retrieved, will never be null * @param authTokenType the type of auth token to retrieve, will never be null * @param options a Bundle of authenticator-specific options, may be null - * @return a Bundle result or null if the result is to be returned via the response. The result - * will contain either: - * <ul> - * <li> {@link AccountManager#KEY_INTENT}, or - * <li> {@link AccountManager#KEY_ACCOUNT_NAME}, {@link AccountManager#KEY_ACCOUNT_TYPE}, - * and {@link AccountManager#KEY_AUTHTOKEN}, or - * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to - * indicate an error - * </ul> + * @return a Bundle result or null if the result is to be returned via the response. * @throws NetworkErrorException if the authenticator could not honor the request due to a * network error */ @@ -518,6 +557,7 @@ public abstract class AbstractAccountAuthenticator { public Bundle getAccountCredentialsForCloning(final AccountAuthenticatorResponse response, final Account account) throws NetworkErrorException { new Thread(new Runnable() { + @Override public void run() { Bundle result = new Bundle(); result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); @@ -543,6 +583,7 @@ public abstract class AbstractAccountAuthenticator { Account account, Bundle accountCredentials) throws NetworkErrorException { new Thread(new Runnable() { + @Override public void run() { Bundle result = new Bundle(); result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index d214a20..3315c89 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -17,6 +17,7 @@ package com.android.server.accounts; import android.Manifest; +import android.accounts.AbstractAccountAuthenticator; import android.accounts.Account; import android.accounts.AccountAndUser; import android.accounts.AccountAuthenticatorResponse; @@ -49,6 +50,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.RegisteredServicesCache; import android.content.pm.RegisteredServicesCacheListener; import android.content.pm.ResolveInfo; +import android.content.pm.Signature; import android.content.pm.UserInfo; import android.database.Cursor; import android.database.DatabaseUtils; @@ -84,6 +86,11 @@ import com.google.android.collect.Sets; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -93,6 +100,7 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -166,6 +174,10 @@ public class AccountManagerService private static final String[] ACCOUNT_TYPE_COUNT_PROJECTION = new String[] { ACCOUNTS_TYPE, ACCOUNTS_TYPE_COUNT}; private static final Intent ACCOUNTS_CHANGED_INTENT; + static { + ACCOUNTS_CHANGED_INTENT = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION); + ACCOUNTS_CHANGED_INTENT.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + } private static final String COUNT_OF_MATCHING_GRANTS = "" + "SELECT COUNT(*) FROM " + TABLE_GRANTS + ", " + TABLE_ACCOUNTS @@ -177,6 +189,7 @@ public class AccountManagerService private static final String SELECTION_AUTHTOKENS_BY_ACCOUNT = AUTHTOKENS_ACCOUNTS_ID + "=(select _id FROM accounts WHERE name=? AND type=?)"; + private static final String[] COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN = {AUTHTOKENS_TYPE, AUTHTOKENS_AUTHTOKEN}; @@ -205,6 +218,10 @@ public class AccountManagerService /** protected by the {@link #cacheLock} */ private final HashMap<Account, HashMap<String, String>> authTokenCache = new HashMap<Account, HashMap<String, String>>(); + + /** protected by the {@link #cacheLock} */ + private final HashMap<Account, WeakReference<TokenCache>> accountTokenCaches = new HashMap<>(); + /** * protected by the {@link #cacheLock} * @@ -237,12 +254,6 @@ public class AccountManagerService new AtomicReference<AccountManagerService>(); private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[]{}; - static { - ACCOUNTS_CHANGED_INTENT = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION); - ACCOUNTS_CHANGED_INTENT.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - } - - /** * This should only be called by system code. One should only call this after the service * has started. @@ -425,6 +436,7 @@ public class AccountManagerService final Account account = new Account(accountName, accountType); accounts.userDataCache.remove(account); accounts.authTokenCache.remove(account); + accounts.accountTokenCaches.remove(account); } else { ArrayList<String> accountNames = accountNamesByType.get(accountType); if (accountNames == null) { @@ -1337,9 +1349,10 @@ public class AccountManagerService @Override public void invalidateAuthToken(String accountType, String authToken) { + int callerUid = Binder.getCallingUid(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "invalidateAuthToken: accountType " + accountType - + ", caller's uid " + Binder.getCallingUid() + + ", caller's uid " + callerUid + ", pid " + Binder.getCallingPid()); } if (accountType == null) throw new IllegalArgumentException("accountType is null"); @@ -1353,6 +1366,7 @@ public class AccountManagerService db.beginTransaction(); try { invalidateAuthTokenLocked(accounts, db, accountType, authToken); + invalidateCustomTokenLocked(accounts, accountType, authToken); db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -1363,6 +1377,26 @@ public class AccountManagerService } } + private void invalidateCustomTokenLocked( + UserAccounts accounts, + String accountType, + String authToken) { + if (authToken == null || accountType == null) { + return; + } + // Also wipe out cached token in memory. + for (Account a : accounts.accountTokenCaches.keySet()) { + if (a.type.equals(accountType)) { + WeakReference<TokenCache> tokenCacheRef = + accounts.accountTokenCaches.get(a); + TokenCache cache = null; + if (tokenCacheRef != null && (cache = tokenCacheRef.get()) != null) { + cache.remove(authToken); + } + } + } + } + private void invalidateAuthTokenLocked(UserAccounts accounts, SQLiteDatabase db, String accountType, String authToken) { if (authToken == null || accountType == null) { @@ -1385,14 +1419,41 @@ public class AccountManagerService String accountName = cursor.getString(1); String authTokenType = cursor.getString(2); db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ID + "=" + authTokenId, null); - writeAuthTokenIntoCacheLocked(accounts, db, new Account(accountName, accountType), - authTokenType, null); + writeAuthTokenIntoCacheLocked( + accounts, + db, + new Account(accountName, accountType), + authTokenType, + null); } } finally { cursor.close(); } } + private void saveCachedToken( + UserAccounts accounts, + Account account, + String callerPkg, + byte[] callerSigDigest, + String tokenType, + String token, + long expiryMillis) { + + if (account == null || tokenType == null || callerPkg == null || callerSigDigest == null) { + return; + } + cancelNotification(getSigninRequiredNotificationId(accounts, account), + new UserHandle(accounts.userId)); + synchronized (accounts.cacheLock) { + TokenCache cache = getTokenCacheForAccountLocked(accounts, account); + if (cache != null) { + cache.put(token, tokenType, callerPkg, callerSigDigest, expiryMillis); + } + return; + } + } + private boolean saveAuthTokenToDatabase(UserAccounts accounts, Account account, String type, String authToken) { if (account == null || type == null) { @@ -1510,6 +1571,7 @@ public class AccountManagerService db.update(TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId); db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ACCOUNTS_ID + "=?", argsAccountId); accounts.authTokenCache.remove(account); + accounts.accountTokenCaches.remove(account); db.setTransactionSuccessful(); String action = (password == null || password.length() == 0) ? @@ -1673,9 +1735,14 @@ public class AccountManagerService } @Override - public void getAuthToken(IAccountManagerResponse response, final Account account, - final String authTokenType, final boolean notifyOnAuthFailure, - final boolean expectActivityLaunch, Bundle loginOptionsIn) { + public void getAuthToken( + IAccountManagerResponse response, + final Account account, + final String authTokenType, + final boolean notifyOnAuthFailure, + final boolean expectActivityLaunch, + final Bundle loginOptions) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "getAuthToken: " + account + ", response " + response @@ -1707,19 +1774,33 @@ public class AccountManagerService final RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo; authenticatorInfo = mAuthenticatorCache.getServiceInfo( AuthenticatorDescription.newKey(account.type), accounts.userId); + final boolean customTokens = - authenticatorInfo != null && authenticatorInfo.type.customTokens; + authenticatorInfo != null && authenticatorInfo.type.customTokens; // skip the check if customTokens final int callerUid = Binder.getCallingUid(); final boolean permissionGranted = customTokens || permissionIsGranted(account, authTokenType, callerUid); - final Bundle loginOptions = (loginOptionsIn == null) ? new Bundle() : - loginOptionsIn; + // Get the calling package. We will use it for the purpose of caching. + final String callerPkg = loginOptions.getString(AccountManager.KEY_ANDROID_PACKAGE_NAME); + List<String> callerOwnedPackageNames = Arrays.asList(mPackageManager.getPackagesForUid(callerUid)); + if (callerPkg == null || !callerOwnedPackageNames.contains(callerPkg)) { + String msg = String.format( + "Uid %s is attempting to illegally masquerade as package %s!", + callerUid, + callerPkg); + throw new SecurityException(msg); + } + // let authenticator know the identity of the caller loginOptions.putInt(AccountManager.KEY_CALLER_UID, callerUid); loginOptions.putInt(AccountManager.KEY_CALLER_PID, Binder.getCallingPid()); + + // Distill the caller's package signatures into a single digest. + final byte[] callerPkgSigDigest = calculatePackageSignatureDigest(callerPkg); + if (notifyOnAuthFailure) { loginOptions.putBoolean(AccountManager.KEY_NOTIFY_ON_FAILURE, true); } @@ -1740,6 +1821,28 @@ public class AccountManagerService } } + if (customTokens) { + /* + * Look up tokens in the new cache only if the loginOptions don't have parameters + * outside of those expected to be injected by the AccountManager, e.g. + * ANDORID_PACKAGE_NAME. + */ + String token = readCachedTokenInternal( + accounts, + account, + authTokenType, + callerPkg, + callerPkgSigDigest); + if (token != null) { + Bundle result = new Bundle(); + result.putString(AccountManager.KEY_AUTHTOKEN, token); + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + onResult(response, result); + return; + } + } + new Session(accounts, response, account.type, expectActivityLaunch, false /* stripAuthTokenFromResult */, account.name, false /* authDetailsRequired */) { @@ -1786,9 +1889,26 @@ public class AccountManagerService "the type and name should not be empty"); return; } + Account resultAccount = new Account(name, type); if (!customTokens) { - saveAuthTokenToDatabase(mAccounts, new Account(name, type), - authTokenType, authToken); + saveAuthTokenToDatabase( + mAccounts, + resultAccount, + authTokenType, + authToken); + } + long expiryMillis = result.getLong( + AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, 0L); + if (customTokens + && expiryMillis > System.currentTimeMillis()) { + saveCachedToken( + mAccounts, + account, + callerPkg, + callerPkgSigDigest, + authTokenType, + authToken, + expiryMillis); } } @@ -1807,6 +1927,25 @@ public class AccountManagerService } } + private byte[] calculatePackageSignatureDigest(String callerPkg) { + MessageDigest digester; + try { + digester = MessageDigest.getInstance("SHA-256"); + PackageInfo pkgInfo = mPackageManager.getPackageInfo( + callerPkg, PackageManager.GET_SIGNATURES); + for (Signature sig : pkgInfo.signatures) { + digester.update(sig.toByteArray()); + } + } catch (NoSuchAlgorithmException x) { + Log.wtf(TAG, "SHA-256 should be available", x); + digester = null; + } catch (NameNotFoundException e) { + Log.w(TAG, "Could not find packageinfo for: " + callerPkg); + digester = null; + } + return (digester == null) ? null : digester.digest(); + } + private void createNoCredentialsPermissionNotification(Account account, Intent intent, int userId) { int uid = intent.getIntExtra( @@ -2745,13 +2884,13 @@ public class AccountManagerService if (result != null) { boolean isSuccessfulConfirmCreds = result.getBoolean( AccountManager.KEY_BOOLEAN_RESULT, false); - boolean isSuccessfulUpdateCreds = + boolean isSuccessfulUpdateCreds = result.containsKey(AccountManager.KEY_ACCOUNT_NAME) && result.containsKey(AccountManager.KEY_ACCOUNT_TYPE); - // We should only update lastAuthenticated time, if + // We should only update lastAuthenticated time, if // mUpdateLastAuthenticatedTime is true and the confirmRequest // or updateRequest was successful - boolean needUpdate = mUpdateLastAuthenticatedTime + boolean needUpdate = mUpdateLastAuthenticatedTime && (isSuccessfulConfirmCreds || isSuccessfulUpdateCreds); if (needUpdate || mAuthDetailsRequired) { boolean accountPresent = isAccountPresentForCaller(mAccountName, mAccountType); @@ -3398,7 +3537,6 @@ public class AccountManagerService return; } } - String msg = "caller uid " + uid + " lacks any of " + TextUtils.join(",", permissions); Log.w(TAG, " " + msg); throw new SecurityException(msg); @@ -3796,6 +3934,18 @@ public class AccountManagerService } } + protected String readCachedTokenInternal( + UserAccounts accounts, + Account account, + String tokenType, + String callingPackage, + byte[] pkgSigDigest) { + synchronized (accounts.cacheLock) { + TokenCache cache = getTokenCacheForAccountLocked(accounts, account); + return cache.get(tokenType, callingPackage, pkgSigDigest); + } + } + protected void writeAuthTokenIntoCacheLocked(UserAccounts accounts, final SQLiteDatabase db, Account account, String key, String value) { HashMap<String, String> authTokensForAccount = accounts.authTokenCache.get(account); @@ -3877,6 +4027,17 @@ public class AccountManagerService return authTokensForAccount; } + protected TokenCache getTokenCacheForAccountLocked(UserAccounts accounts, Account account) { + WeakReference<TokenCache> cacheRef = accounts.accountTokenCaches.get(account); + TokenCache cache; + if (cacheRef == null || (cache = cacheRef.get()) == null) { + cache = new TokenCache(); + cacheRef = new WeakReference<>(cache); + accounts.accountTokenCaches.put(account, cacheRef); + } + return cache; + } + private Context getContextForUser(UserHandle user) { try { return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user); diff --git a/services/core/java/com/android/server/accounts/TokenCache.java b/services/core/java/com/android/server/accounts/TokenCache.java new file mode 100644 index 0000000..70a7010 --- /dev/null +++ b/services/core/java/com/android/server/accounts/TokenCache.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2015 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 com.android.server.accounts; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; + +/** + * TokenCaches manage tokens associated with an account in memory. + */ +/* default */ class TokenCache { + + private static class Value { + public final String token; + public final long expiryEpochMillis; + + public Value(String token, long expiryEpochMillis) { + this.token = token; + this.expiryEpochMillis = expiryEpochMillis; + } + } + + private static class Key { + public final String packageName; + public final String tokenType; + public final byte[] sigDigest; + + public Key(String tokenType, String packageName, byte[] sigDigest) { + this.tokenType = tokenType; + this.packageName = packageName; + this.sigDigest = sigDigest; + } + + @Override + public boolean equals(Object o) { + if (o != null && o instanceof Key) { + Key cacheKey = (Key) o; + return Objects.equals(packageName, cacheKey.packageName) + && Objects.equals(tokenType, cacheKey.tokenType) + && Arrays.equals(sigDigest, cacheKey.sigDigest); + } else { + return false; + } + } + + @Override + public int hashCode() { + return packageName.hashCode() ^ tokenType.hashCode() ^ Arrays.hashCode(sigDigest); + } + } + + /** + * Map associating basic token lookup information with with actual tokens (and optionally their + * expiration times). + */ + private HashMap<Key, Value> mCachedTokens = new HashMap<>(); + + /** + * Map associated tokens with an Evictor that will manage evicting the token from the cache. + * This reverse lookup is needed because very little information is given at token invalidation + * time. + */ + private HashMap<String, Evictor> mTokenEvictors = new HashMap<>(); + + private class Evictor { + private final String mToken; + private final List<Key> mKeys; + + public Evictor(String token) { + mKeys = new ArrayList<>(); + mToken = token; + } + + public void add(Key k) { + mKeys.add(k); + } + + public void evict() { + for (Key k : mKeys) { + mCachedTokens.remove(k); + } + // Clear out the evictor reference. + mTokenEvictors.remove(mToken); + } + } + + /** + * Caches the specified token until the specified expiryMillis. The token will be associated + * with the given token type, package name, and digest of signatures. + * + * @param token + * @param tokenType + * @param packageName + * @param sigDigest + * @param expiryMillis + */ + public void put( + String token, + String tokenType, + String packageName, + byte[] sigDigest, + long expiryMillis) { + if (token == null || System.currentTimeMillis() > expiryMillis) { + return; + } + Key k = new Key(tokenType, packageName, sigDigest); + // Prep evictor. No token should be cached without a corresponding evictor. + Evictor evictor = mTokenEvictors.get(token); + if (evictor == null) { + evictor = new Evictor(token); + } + evictor.add(k); + mTokenEvictors.put(token, evictor); + // Then cache values. + Value v = new Value(token, expiryMillis); + mCachedTokens.put(k, v); + } + + /** + * Evicts the specified token from the cache. This should be called as part of a token + * invalidation workflow. + */ + public void remove(String token) { + Evictor evictor = mTokenEvictors.get(token); + if (evictor == null) { + // This condition is expected if the token isn't cached. + return; + } + evictor.evict(); + } + + /** + * Gets a token from the cache if possible. + */ + public String get(String tokenType, String packageName, byte[] sigDigest) { + Key k = new Key(tokenType, packageName, sigDigest); + Value v = mCachedTokens.get(k); + long currentTime = System.currentTimeMillis(); + if (v != null && currentTime < v.expiryEpochMillis) { + return v.token; + } else if (v != null) { + remove(v.token); + } + return null; + } +} |