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 java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Objects;
28 
29 /**
30  * TokenCaches manage time limited authentication tokens in memory.
31  */
32 /* default */ class TokenCache {
33 
34     private static final int MAX_CACHE_CHARS = 64000;
35 
36     /** Package private*/
37     static class Value {
38         public final String token;
39         public final long expiryEpochMillis;
40 
Value(String token, long expiryEpochMillis)41         public Value(String token, long expiryEpochMillis) {
42             this.token = token;
43             this.expiryEpochMillis = expiryEpochMillis;
44         }
45     }
46 
47     private static class Key {
48         public final Account account;
49         public final String packageName;
50         public final String tokenType;
51         public final byte[] sigDigest;
52 
Key(Account account, String tokenType, String packageName, byte[] sigDigest)53         public Key(Account account, String tokenType, String packageName, byte[] sigDigest) {
54             this.account = account;
55             this.tokenType = tokenType;
56             this.packageName = packageName;
57             this.sigDigest = sigDigest;
58         }
59 
60         @Override
equals(Object o)61         public boolean equals(Object o) {
62             if (o != null && o instanceof Key) {
63                 Key cacheKey = (Key) o;
64                 return Objects.equals(account, cacheKey.account)
65                         && Objects.equals(packageName, cacheKey.packageName)
66                         && Objects.equals(tokenType, cacheKey.tokenType)
67                         && Arrays.equals(sigDigest, cacheKey.sigDigest);
68             } else {
69                 return false;
70             }
71         }
72 
73         @Override
hashCode()74         public int hashCode() {
75             return account.hashCode()
76                     ^ packageName.hashCode()
77                     ^ tokenType.hashCode()
78                     ^ Arrays.hashCode(sigDigest);
79         }
80     }
81 
82     private static class TokenLruCache extends LruCache<Key, Value> {
83 
84         private class Evictor {
85             private final List<Key> mKeys;
86 
Evictor()87             public Evictor() {
88                 mKeys = new ArrayList<>();
89             }
90 
add(Key k)91             public void add(Key k) {
92                 mKeys.add(k);
93             }
94 
evict()95             public void evict() {
96                 for (Key k : mKeys) {
97                     TokenLruCache.this.remove(k);
98                 }
99             }
100         }
101 
102         /**
103          * Map associated tokens with an Evictor that will manage evicting the token from the
104          * cache. This reverse lookup is needed because very little information is given at token
105          * invalidation time.
106          */
107         private HashMap<Pair<String, String>, Evictor> mTokenEvictors = new HashMap<>();
108         private HashMap<Account, Evictor> mAccountEvictors = new HashMap<>();
109 
TokenLruCache()110         public TokenLruCache() {
111             super(MAX_CACHE_CHARS);
112         }
113 
114         @Override
sizeOf(Key k, Value v)115         protected int sizeOf(Key k, Value v) {
116             return v.token.length();
117         }
118 
119         @Override
entryRemoved(boolean evicted, Key k, Value oldVal, Value newVal)120         protected void entryRemoved(boolean evicted, Key k, Value oldVal, Value newVal) {
121             // When a token has been removed, clean up the associated Evictor.
122             if (oldVal != null && newVal == null) {
123                 /*
124                  * This is recursive, but it won't spiral out of control because LruCache is
125                  * thread safe and the Evictor can only be removed once.
126                  */
127                 Evictor evictor = mTokenEvictors.remove(new Pair<>(k.account.type, oldVal.token));
128                 if (evictor != null) {
129                     evictor.evict();
130                 }
131             }
132         }
133 
putToken(Key k, Value v)134         public void putToken(Key k, Value v) {
135             // Prepare for removal by token string.
136             Pair<String, String> mapKey = new Pair<>(k.account.type, v.token);
137             Evictor tokenEvictor = mTokenEvictors.get(mapKey);
138             if (tokenEvictor == null) {
139                 tokenEvictor = new Evictor();
140             }
141             tokenEvictor.add(k);
142             mTokenEvictors.put(mapKey, 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, accountEvictor);
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         Objects.requireNonNull(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 Value 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;
225         } else if (v != null) {
226             remove(account.type, v.token);
227         }
228         return null;
229     }
230 }
231