/* * 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 android.accounts.Account; import android.util.LruCache; import android.util.Pair; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Objects; /** * TokenCaches manage time limited authentication tokens in memory. */ /* default */ class TokenCache { private static final int MAX_CACHE_CHARS = 64000; 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 Account account; public final String packageName; public final String tokenType; public final byte[] sigDigest; public Key(Account account, String tokenType, String packageName, byte[] sigDigest) { this.account = account; 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(account, cacheKey.account) && Objects.equals(packageName, cacheKey.packageName) && Objects.equals(tokenType, cacheKey.tokenType) && Arrays.equals(sigDigest, cacheKey.sigDigest); } else { return false; } } @Override public int hashCode() { return account.hashCode() ^ packageName.hashCode() ^ tokenType.hashCode() ^ Arrays.hashCode(sigDigest); } } private static class TokenLruCache extends LruCache { private class Evictor { private final List mKeys; public Evictor() { mKeys = new ArrayList<>(); } public void add(Key k) { mKeys.add(k); } public void evict() { for (Key k : mKeys) { TokenLruCache.this.remove(k); } } } /** * 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, Evictor> mTokenEvictors = new HashMap<>(); private HashMap mAccountEvictors = new HashMap<>(); public TokenLruCache() { super(MAX_CACHE_CHARS); } @Override protected int sizeOf(Key k, Value v) { return v.token.length(); } @Override protected void entryRemoved(boolean evicted, Key k, Value oldVal, Value newVal) { // When a token has been removed, clean up the associated Evictor. if (oldVal != null && newVal == null) { /* * This is recursive, but it won't spiral out of control because LruCache is * thread safe and the Evictor can only be removed once. */ Evictor evictor = mTokenEvictors.remove(oldVal.token); if (evictor != null) { evictor.evict(); } } } public void putToken(Key k, Value v) { // Prepare for removal by token string. Evictor tokenEvictor = mTokenEvictors.get(v.token); if (tokenEvictor == null) { tokenEvictor = new Evictor(); } tokenEvictor.add(k); mTokenEvictors.put(new Pair<>(k.account.type, v.token), tokenEvictor); // Prepare for removal by associated account. Evictor accountEvictor = mAccountEvictors.get(k.account); if (accountEvictor == null) { accountEvictor = new Evictor(); } accountEvictor.add(k); mAccountEvictors.put(k.account, tokenEvictor); // Only cache the token once we can remove it directly or by account. put(k, v); } public void evict(String accountType, String token) { Evictor evictor = mTokenEvictors.get(new Pair<>(accountType, token)); if (evictor != null) { evictor.evict(); } } public void evict(Account account) { Evictor evictor = mAccountEvictors.get(account); if (evictor != null) { evictor.evict(); } } } /** * Map associating basic token lookup information with with actual tokens (and optionally their * expiration times). */ private TokenLruCache mCachedTokens = new TokenLruCache(); /** * 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( Account account, String token, String tokenType, String packageName, byte[] sigDigest, long expiryMillis) { Preconditions.checkNotNull(account); if (token == null || System.currentTimeMillis() > expiryMillis) { return; } Key k = new Key(account, tokenType, packageName, sigDigest); Value v = new Value(token, expiryMillis); mCachedTokens.putToken(k, v); } /** * Evicts the specified token from the cache. This should be called as part of a token * invalidation workflow. */ public void remove(String accountType, String token) { mCachedTokens.evict(accountType, token); } public void remove(Account account) { mCachedTokens.evict(account); } /** * Gets a token from the cache if possible. */ public String get(Account account, String tokenType, String packageName, byte[] sigDigest) { Key k = new Key(account, 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(account.type, v.token); } return null; } }