1 /*
2  * Copyright (C) 2022 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.credentials;
18 
19 import android.credentials.CredentialDescription;
20 import android.credentials.RegisterCredentialDescriptionRequest;
21 import android.credentials.UnregisterCredentialDescriptionRequest;
22 import android.service.credentials.CredentialEntry;
23 import android.util.SparseArray;
24 
25 import com.android.internal.annotations.GuardedBy;
26 import com.android.internal.annotations.VisibleForTesting;
27 
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.concurrent.locks.ReentrantLock;
34 
35 /** Contains information on what CredentialProvider has what provisioned Credential. */
36 public class CredentialDescriptionRegistry {
37 
38     private static final int MAX_ALLOWED_CREDENTIAL_DESCRIPTIONS = 128;
39     private static final int MAX_ALLOWED_ENTRIES_PER_PROVIDER = 16;
40     @GuardedBy("sLock")
41     private static final SparseArray<CredentialDescriptionRegistry>
42             sCredentialDescriptionSessionPerUser;
43     private static final ReentrantLock sLock;
44 
45     static {
46         sCredentialDescriptionSessionPerUser = new SparseArray<>();
47         sLock = new ReentrantLock();
48     }
49 
50     /** Represents the results of a given query into the registry. */
51     public static final class FilterResult {
52         final String mPackageName;
53         final Set<String> mElementKeys;
54         final List<CredentialEntry> mCredentialEntries;
55 
56         @VisibleForTesting
FilterResult(String packageName, Set<String> elementKeys, List<CredentialEntry> credentialEntries)57         FilterResult(String packageName,
58                 Set<String> elementKeys,
59                 List<CredentialEntry> credentialEntries) {
60             mPackageName = packageName;
61             mElementKeys = elementKeys;
62             mCredentialEntries = credentialEntries;
63         }
64     }
65 
66     /** Get and/or create a {@link  CredentialDescription} for the given user id. */
67     @GuardedBy("sLock")
forUser(int userId)68     public static CredentialDescriptionRegistry forUser(int userId) {
69         sLock.lock();
70         try {
71             CredentialDescriptionRegistry session =
72                     sCredentialDescriptionSessionPerUser.get(userId, null);
73 
74             if (session == null) {
75                 session = new CredentialDescriptionRegistry();
76                 sCredentialDescriptionSessionPerUser.put(userId, session);
77             }
78             return session;
79         } finally {
80             sLock.unlock();
81         }
82     }
83 
84     /** Clears an existing session for a given user identifier. */
85     @GuardedBy("sLock")
clearUserSession(int userId)86     public static void clearUserSession(int userId) {
87         sLock.lock();
88         try {
89             sCredentialDescriptionSessionPerUser.remove(userId);
90         } finally {
91             sLock.unlock();
92         }
93     }
94 
95     /** Clears an existing session for a given user identifier. Used when testing only. */
96     @GuardedBy("sLock")
97     @VisibleForTesting
clearAllSessions()98     static void clearAllSessions() {
99         sLock.lock();
100         try {
101             sCredentialDescriptionSessionPerUser.clear();
102         } finally {
103             sLock.unlock();
104         }
105     }
106 
107     /** Sets an existing session for a given user identifier. Used when testing only. */
108     @GuardedBy("sLock")
109     @VisibleForTesting
setSession(int userId, CredentialDescriptionRegistry credentialDescriptionRegistry)110     static void setSession(int userId, CredentialDescriptionRegistry
111             credentialDescriptionRegistry) {
112         sLock.lock();
113         try {
114             sCredentialDescriptionSessionPerUser.put(userId, credentialDescriptionRegistry);
115         } finally {
116             sLock.unlock();
117         }
118     }
119 
120     private Map<String, Set<CredentialDescription>> mCredentialDescriptions;
121     private int mTotalDescriptionCount;
122 
CredentialDescriptionRegistry()123     private CredentialDescriptionRegistry() {
124         this.mCredentialDescriptions = new HashMap<>();
125         this.mTotalDescriptionCount = 0;
126     }
127 
128     /** Handle the given {@link RegisterCredentialDescriptionRequest} by creating
129      * the appropriate package name mapping. */
executeRegisterRequest(RegisterCredentialDescriptionRequest request, String callingPackageName)130     public void executeRegisterRequest(RegisterCredentialDescriptionRequest request,
131             String callingPackageName) {
132 
133         if (!mCredentialDescriptions.containsKey(callingPackageName)) {
134             mCredentialDescriptions.put(callingPackageName, new HashSet<>());
135         }
136 
137         if (mTotalDescriptionCount <= MAX_ALLOWED_CREDENTIAL_DESCRIPTIONS
138                 && mCredentialDescriptions.get(callingPackageName).size()
139                 <= MAX_ALLOWED_ENTRIES_PER_PROVIDER) {
140             Set<CredentialDescription> descriptions = request.getCredentialDescriptions();
141             int size = mCredentialDescriptions.get(callingPackageName).size();
142             mCredentialDescriptions.get(callingPackageName)
143                     .addAll(descriptions);
144             mTotalDescriptionCount += mCredentialDescriptions.get(callingPackageName).size() - size;
145         }
146 
147     }
148 
149     /** Handle the given {@link UnregisterCredentialDescriptionRequest} by creating
150      * the appropriate package name mapping. */
executeUnregisterRequest( UnregisterCredentialDescriptionRequest request, String callingPackageName)151     public void executeUnregisterRequest(
152             UnregisterCredentialDescriptionRequest request,
153             String callingPackageName) {
154 
155         if (mCredentialDescriptions.containsKey(callingPackageName)) {
156             int size = mCredentialDescriptions.get(callingPackageName).size();
157             mCredentialDescriptions.get(callingPackageName)
158                     .removeAll(request.getCredentialDescriptions());
159             mTotalDescriptionCount -= size - mCredentialDescriptions.get(callingPackageName).size();
160         }
161     }
162 
163     /** Returns package names and entries of a CredentialProviders that can satisfy a given
164      * {@link CredentialDescription}. */
getFilteredResultForProvider(String packageName, Set<String> requestedKeyElements)165     public Set<FilterResult> getFilteredResultForProvider(String packageName,
166             Set<String> requestedKeyElements) {
167         Set<FilterResult> result = new HashSet<>();
168         if (!mCredentialDescriptions.containsKey(packageName)) {
169             return result;
170         }
171         Set<CredentialDescription> currentSet = mCredentialDescriptions.get(packageName);
172         for (CredentialDescription containedDescription: currentSet) {
173             if (checkForMatch(containedDescription.getSupportedElementKeys(),
174                     requestedKeyElements)) {
175                 result.add(new FilterResult(packageName,
176                         containedDescription.getSupportedElementKeys(), containedDescription
177                         .getCredentialEntries()));
178             }
179         }
180         return result;
181     }
182 
183     /** Returns package names of CredentialProviders that can satisfy a given
184      * {@link CredentialDescription}. */
getMatchingProviders(Set<Set<String>> supportedElementKeys)185     public Set<FilterResult> getMatchingProviders(Set<Set<String>> supportedElementKeys) {
186         Set<FilterResult> result = new HashSet<>();
187         for (String packageName: mCredentialDescriptions.keySet()) {
188             Set<CredentialDescription> currentSet = mCredentialDescriptions.get(packageName);
189             for (CredentialDescription containedDescription : currentSet) {
190                 if (canProviderSatisfyAny(containedDescription.getSupportedElementKeys(),
191                         supportedElementKeys)) {
192                     result.add(new FilterResult(packageName,
193                             containedDescription.getSupportedElementKeys(), containedDescription
194                             .getCredentialEntries()));
195                 }
196             }
197         }
198         return result;
199     }
200 
evictProviderWithPackageName(String packageName)201     void evictProviderWithPackageName(String packageName) {
202         if (mCredentialDescriptions.containsKey(packageName)) {
203             mCredentialDescriptions.remove(packageName);
204         }
205     }
206 
canProviderSatisfyAny(Set<String> registeredElementKeys, Set<Set<String>> requestedElementKeys)207     private static boolean canProviderSatisfyAny(Set<String> registeredElementKeys,
208             Set<Set<String>> requestedElementKeys) {
209         for (Set<String> requestedUnflattenedString : requestedElementKeys) {
210             if (registeredElementKeys.containsAll(requestedUnflattenedString)) {
211                 return true;
212             }
213         }
214         return false;
215     }
216 
checkForMatch(Set<String> registeredElementKeys, Set<String> requestedElementKeys)217     static boolean checkForMatch(Set<String> registeredElementKeys,
218             Set<String> requestedElementKeys) {
219         return registeredElementKeys.containsAll(requestedElementKeys);
220     }
221 
222 }
223