content: /*
   * 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<Key, Value> {
  
          private class Evictor {
              private final List<Key> 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<Pair<String, String>, Evictor> mTokenEvictors = new HashMap<>();
          private HashMap<Account, Evictor> 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(new Pair<>(k.account.type, oldVal.token));
                  if (evictor != null) {
                      evictor.evict();
                  }
              }
          }
  
          public void putToken(Key k, Value v) {
              // Prepare for removal by token string.
              Pair<String, String> mapKey = new Pair<>(k.account.type, v.token);
              Evictor tokenEvictor = mTokenEvictors.get(mapKey);
              if (tokenEvictor == null) {
                  tokenEvictor = new Evictor();
              }
              tokenEvictor.add(k);
              mTokenEvictors.put(mapKey, 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;
      }
  }
  