summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/current.txt1
-rw-r--r--api/system-current.txt1
-rw-r--r--core/java/android/accounts/AbstractAccountAuthenticator.java61
-rw-r--r--services/core/java/com/android/server/accounts/AccountManagerService.java203
-rw-r--r--services/core/java/com/android/server/accounts/TokenCache.java163
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;
+ }
+}