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