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
17package com.android.server.accounts;
18
19import android.accounts.Account;
20import android.util.LruCache;
21import android.util.Pair;
22
23import com.android.internal.util.Preconditions;
24
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.HashMap;
28import java.util.List;
29import 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
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
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
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
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
88            public Evictor() {
89                mKeys = new ArrayList<>();
90            }
91
92            public void add(Key k) {
93                mKeys.add(k);
94            }
95
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
111        public TokenLruCache() {
112            super(MAX_CACHE_CHARS);
113        }
114
115        @Override
116        protected int sizeOf(Key k, Value v) {
117            return v.token.length();
118        }
119
120        @Override
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
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
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
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     */
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     */
208    public void remove(String accountType, String token) {
209        mCachedTokens.evict(accountType, token);
210    }
211
212    public void remove(Account account) {
213        mCachedTokens.evict(account);
214    }
215
216    /**
217     * Gets a token from the cache if possible.
218     */
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