1 /* 2 * Copyright (C) 2015 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 17 package com.android.server.accounts; 18 19 import android.accounts.Account; 20 import android.util.LruCache; 21 import android.util.Pair; 22 23 import com.android.internal.util.Preconditions; 24 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.HashMap; 28 import java.util.List; 29 import java.util.Objects; 30 31 /** 32 * TokenCaches manage time limited authentication tokens in memory. 33 */ 34 /* default */ class TokenCache { 35 36 private static final int MAX_CACHE_CHARS = 64000; 37 38 private static class Value { 39 public final String token; 40 public final long expiryEpochMillis; 41 Value(String token, long expiryEpochMillis)42 public Value(String token, long expiryEpochMillis) { 43 this.token = token; 44 this.expiryEpochMillis = expiryEpochMillis; 45 } 46 } 47 48 private static class Key { 49 public final Account account; 50 public final String packageName; 51 public final String tokenType; 52 public final byte[] sigDigest; 53 Key(Account account, String tokenType, String packageName, byte[] sigDigest)54 public Key(Account account, String tokenType, String packageName, byte[] sigDigest) { 55 this.account = account; 56 this.tokenType = tokenType; 57 this.packageName = packageName; 58 this.sigDigest = sigDigest; 59 } 60 61 @Override equals(Object o)62 public boolean equals(Object o) { 63 if (o != null && o instanceof Key) { 64 Key cacheKey = (Key) o; 65 return Objects.equals(account, cacheKey.account) 66 && Objects.equals(packageName, cacheKey.packageName) 67 && Objects.equals(tokenType, cacheKey.tokenType) 68 && Arrays.equals(sigDigest, cacheKey.sigDigest); 69 } else { 70 return false; 71 } 72 } 73 74 @Override hashCode()75 public int hashCode() { 76 return account.hashCode() 77 ^ packageName.hashCode() 78 ^ tokenType.hashCode() 79 ^ Arrays.hashCode(sigDigest); 80 } 81 } 82 83 private static class TokenLruCache extends LruCache<Key, Value> { 84 85 private class Evictor { 86 private final List<Key> mKeys; 87 Evictor()88 public Evictor() { 89 mKeys = new ArrayList<>(); 90 } 91 add(Key k)92 public void add(Key k) { 93 mKeys.add(k); 94 } 95 evict()96 public void evict() { 97 for (Key k : mKeys) { 98 TokenLruCache.this.remove(k); 99 } 100 } 101 } 102 103 /** 104 * Map associated tokens with an Evictor that will manage evicting the token from the 105 * cache. This reverse lookup is needed because very little information is given at token 106 * invalidation time. 107 */ 108 private HashMap<Pair<String, String>, Evictor> mTokenEvictors = new HashMap<>(); 109 private HashMap<Account, Evictor> mAccountEvictors = new HashMap<>(); 110 TokenLruCache()111 public TokenLruCache() { 112 super(MAX_CACHE_CHARS); 113 } 114 115 @Override sizeOf(Key k, Value v)116 protected int sizeOf(Key k, Value v) { 117 return v.token.length(); 118 } 119 120 @Override entryRemoved(boolean evicted, Key k, Value oldVal, Value newVal)121 protected void entryRemoved(boolean evicted, Key k, Value oldVal, Value newVal) { 122 // When a token has been removed, clean up the associated Evictor. 123 if (oldVal != null && newVal == null) { 124 /* 125 * This is recursive, but it won't spiral out of control because LruCache is 126 * thread safe and the Evictor can only be removed once. 127 */ 128 Evictor evictor = mTokenEvictors.remove(oldVal.token); 129 if (evictor != null) { 130 evictor.evict(); 131 } 132 } 133 } 134 putToken(Key k, Value v)135 public void putToken(Key k, Value v) { 136 // Prepare for removal by token string. 137 Evictor tokenEvictor = mTokenEvictors.get(v.token); 138 if (tokenEvictor == null) { 139 tokenEvictor = new Evictor(); 140 } 141 tokenEvictor.add(k); 142 mTokenEvictors.put(new Pair<>(k.account.type, v.token), tokenEvictor); 143 144 // Prepare for removal by associated account. 145 Evictor accountEvictor = mAccountEvictors.get(k.account); 146 if (accountEvictor == null) { 147 accountEvictor = new Evictor(); 148 } 149 accountEvictor.add(k); 150 mAccountEvictors.put(k.account, tokenEvictor); 151 152 // Only cache the token once we can remove it directly or by account. 153 put(k, v); 154 } 155 evict(String accountType, String token)156 public void evict(String accountType, String token) { 157 Evictor evictor = mTokenEvictors.get(new Pair<>(accountType, token)); 158 if (evictor != null) { 159 evictor.evict(); 160 } 161 162 } 163 evict(Account account)164 public void evict(Account account) { 165 Evictor evictor = mAccountEvictors.get(account); 166 if (evictor != null) { 167 evictor.evict(); 168 } 169 } 170 } 171 172 /** 173 * Map associating basic token lookup information with with actual tokens (and optionally their 174 * expiration times). 175 */ 176 private TokenLruCache mCachedTokens = new TokenLruCache(); 177 178 /** 179 * Caches the specified token until the specified expiryMillis. The token will be associated 180 * with the given token type, package name, and digest of signatures. 181 * 182 * @param token 183 * @param tokenType 184 * @param packageName 185 * @param sigDigest 186 * @param expiryMillis 187 */ put( Account account, String token, String tokenType, String packageName, byte[] sigDigest, long expiryMillis)188 public void put( 189 Account account, 190 String token, 191 String tokenType, 192 String packageName, 193 byte[] sigDigest, 194 long expiryMillis) { 195 Preconditions.checkNotNull(account); 196 if (token == null || System.currentTimeMillis() > expiryMillis) { 197 return; 198 } 199 Key k = new Key(account, tokenType, packageName, sigDigest); 200 Value v = new Value(token, expiryMillis); 201 mCachedTokens.putToken(k, v); 202 } 203 204 /** 205 * Evicts the specified token from the cache. This should be called as part of a token 206 * invalidation workflow. 207 */ remove(String accountType, String token)208 public void remove(String accountType, String token) { 209 mCachedTokens.evict(accountType, token); 210 } 211 remove(Account account)212 public void remove(Account account) { 213 mCachedTokens.evict(account); 214 } 215 216 /** 217 * Gets a token from the cache if possible. 218 */ get(Account account, String tokenType, String packageName, byte[] sigDigest)219 public String get(Account account, String tokenType, String packageName, byte[] sigDigest) { 220 Key k = new Key(account, tokenType, packageName, sigDigest); 221 Value v = mCachedTokens.get(k); 222 long currentTime = System.currentTimeMillis(); 223 if (v != null && currentTime < v.expiryEpochMillis) { 224 return v.token; 225 } else if (v != null) { 226 remove(account.type, v.token); 227 } 228 return null; 229 } 230 } 231