1 /*
2  * Copyright (C) 2021 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.internal.net.eap;
18 
19 import static com.android.internal.net.eap.EapAuthenticator.LOG;
20 import static com.android.internal.net.eap.statemachine.EapSimAkaMethodStateMachine.KEY_LEN;
21 import static com.android.internal.net.eap.statemachine.EapSimAkaMethodStateMachine.MASTER_KEY_LENGTH;
22 
23 import android.annotation.Nullable;
24 import android.os.SystemClock;
25 
26 import com.android.internal.annotations.VisibleForTesting;
27 
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.Iterator;
31 import java.util.LinkedHashMap;
32 import java.util.Map;
33 import java.util.concurrent.TimeUnit;
34 
35 /** EapSimAkaIdentityTracker will manage information to support Re-authentication & Pseudonym. */
36 public class EapSimAkaIdentityTracker {
37     private static final String TAG = EapSimAkaIdentityTracker.class.getSimpleName();
38     /** Lifetime of Reauth Info */
39     private static final long REAUTH_INFO_LIFETIME_MILLIS = TimeUnit.HOURS.toMillis(12); // 12Hour
40     /** Quota of Reauth Info */
41     @VisibleForTesting
42     static final int MAX_NUMBER_OF_REAUTH_INFO = 20;
43 
44     /**
45      * The LinkedHashMap to preserve credentials for re-authentication, with concatenation of reauth
46      * ID and permanent ID as the key and ReauthInfo as the value.
47      *
48      * <p>A map entry will be deleted either if the map size reaches the max quota and the
49      * entry is the oldest, or if the ReauthInfo of this entry is found expired when a new
50      * ReauthInfo is registered
51      */
52     private static Map<String, ReauthInfo> sReauthInfoMap =
53             Collections.synchronizedMap(
54                     new LinkedHashMap<String, ReauthInfo>() {
55                         @Override
56                         protected boolean removeEldestEntry(Map.Entry<String, ReauthInfo> eldest) {
57                             LOG.d(
58                                     TAG,
59                                     "Reached MAX_NUMBER_OF_REAUTH_INFO("
60                                             + MAX_NUMBER_OF_REAUTH_INFO
61                                             + ") remove EldestEntry");
62                             return size() > MAX_NUMBER_OF_REAUTH_INFO;
63                         }
64                     });
65 
66     /** ReauthInfo stores information used for re-authentication */
67     public static class ReauthInfo {
68         private final int mReauthCount;
69         private final byte[] mMk;
70         private final byte[] mKeyEncr;
71         private final byte[] mKeyAut;
72 
73         /**
74          * If system elapsed time is larger than mExpiryTimestampElapsedRealtime, ReauthInfo will be
75          * deleted.
76          */
77         private final long mExpiryTimestampElapsedRealtime;
78 
79         /**
80          * Constructor of ReauthInfo
81          *
82          * @param mk : Master key derived at Full auth
83          * @param kEncr : Encryption key derived at Full auth
84          * @param kAut : Authentication key derived at Full auth
85          */
ReauthInfo(int reauthCount, byte[] mk, byte[] kEncr, byte[] kAut)86         private ReauthInfo(int reauthCount, byte[] mk, byte[] kEncr, byte[] kAut) {
87             mReauthCount = reauthCount;
88             mMk = mk.clone();
89             mKeyEncr = kEncr.clone();
90             mKeyAut = kAut.clone();
91             mExpiryTimestampElapsedRealtime =
92                     SystemClock.elapsedRealtime() + REAUTH_INFO_LIFETIME_MILLIS;
93         }
94 
95         @VisibleForTesting
ReauthInfo( int reauthCount, byte[] mk, byte[] kEncr, byte[] kAut, long elapsedExpiryTimeMillis)96         ReauthInfo(
97                 int reauthCount,
98                 byte[] mk,
99                 byte[] kEncr,
100                 byte[] kAut,
101                 long elapsedExpiryTimeMillis) {
102             mReauthCount = reauthCount;
103             mMk = mk;
104             mKeyEncr = kEncr;
105             mKeyAut = kAut;
106             mExpiryTimestampElapsedRealtime =
107                     SystemClock.elapsedRealtime() + elapsedExpiryTimeMillis;
108         }
109 
110         /**
111          * Retrieves reauth count for re-authentication
112          *
113          * @return : reauth count
114          */
getReauthCount()115         public int getReauthCount() {
116             return mReauthCount;
117         }
118 
119         /**
120          * Retrieves Master key for re-authentication
121          *
122          * @return : Master Key
123          */
getMk()124         public byte[] getMk() {
125             return mMk;
126         }
127 
128         /**
129          * Retrieves encryption key for re-authentication
130          *
131          * @return : encryption key
132          */
getKeyEncr()133         public byte[] getKeyEncr() {
134             return mKeyEncr;
135         }
136 
137         /**
138          * Retrieves authentication key for re-authentication
139          *
140          * @return : authentication key
141          */
getKeyAut()142         public byte[] getKeyAut() {
143             return mKeyAut;
144         }
145 
146         /**
147          * Check expiration of this ReauthInfo
148          *
149          * @return : true(not expired)/false(expired)
150          */
isValid()151         public boolean isValid() {
152             return SystemClock.elapsedRealtime() < mExpiryTimestampElapsedRealtime;
153         }
154     }
155 
156     private static class EapSimAkaIdentityTrackerHolder {
157         static final EapSimAkaIdentityTracker INSTANCE = new EapSimAkaIdentityTracker();
158     }
159 
160     /**
161      * Retrieves static EapSimAkaIdentityTracker instance
162      *
163      * @return static EapSimAkaIdentityTracker
164      */
getInstance()165     public static EapSimAkaIdentityTracker getInstance() {
166         return EapSimAkaIdentityTrackerHolder.INSTANCE;
167     }
168 
169     /**
170      * Create ReauthInfo & add it to ReauthInfo Map
171      *
172      * @param reauthId : re-authentication ID
173      * @param permanentId : permanent ID
174      * @param count : re-authentication Count
175      * @param mk : master key derived from full-auth
176      * @param kEncr : encryption key derived from full-auth
177      * @param kAut : authentication derived from full-auth
178      */
registerReauthCredentials( String reauthId, String permanentId, int count, byte[] mk, byte[] kEncr, byte[] kAut)179     public void registerReauthCredentials(
180             String reauthId, String permanentId, int count, byte[] mk, byte[] kEncr, byte[] kAut) {
181         if (mk.length != MASTER_KEY_LENGTH || kEncr.length != KEY_LEN || kAut.length != KEY_LEN) {
182             throw new IllegalArgumentException("Invalid Full auth key len");
183         }
184         ReauthInfo reauthInfo = new ReauthInfo(count, mk, kEncr, kAut);
185         String key = reauthId + permanentId;
186         LOG.d(TAG, "registerReauthCredentials: key" + key + " reauth count:" + count);
187         LOG.d(TAG, "    MK=" + LOG.pii(mk));
188         LOG.d(TAG, "    K_encr=" + LOG.pii(kEncr));
189         LOG.d(TAG, "    K_aut=" + LOG.pii(kAut));
190         sReauthInfoMap.put(key, reauthInfo);
191         garbageCollect();
192     }
193 
194     /**
195      * Add ReauthInfo to ReauthInfo Map. Only test purpose.
196      *
197      * @param reauthInfo : reauth Info
198      */
199     @VisibleForTesting
addReauthInfo(String key, ReauthInfo reauthInfo)200     void addReauthInfo(String key, ReauthInfo reauthInfo) {
201         sReauthInfoMap.put(key, reauthInfo);
202     }
203 
204     /**
205      * Retrieves ReauthInfo with reauthId & permanentId as a key
206      *
207      * @param reauthId : re-authId set by application
208      * @param permanentId : permanentId set by application
209      * @return ReauthInfo : mapped ReauthInfo, return null when there is no matched info.
210      */
211     @Nullable
getReauthInfo(String reauthId, String permanentId)212     public ReauthInfo getReauthInfo(String reauthId, String permanentId) {
213         String key = reauthId + permanentId;
214         ReauthInfo reauthInfo = sReauthInfoMap.get(key);
215         if (reauthInfo == null) {
216             LOG.d(TAG, "getReauthInfo no reauthInfo for key:" + key);
217         }
218         return reauthInfo;
219     }
220 
221     /**
222      * Delete ReauthInfo with reauthId & permanentId as a key
223      *
224      * @param reauthId : re-authId set by application
225      * @param permanentId : permanentId set by application
226      */
deleteReauthInfo(String reauthId, String permanentId)227     public void deleteReauthInfo(String reauthId, String permanentId) {
228         String key = reauthId + permanentId;
229         LOG.d(TAG, "deleteReauthInfo for key:" + key);
230         sReauthInfoMap.remove(key);
231     }
232 
233     /** Clean up the reauthInfoMap */
234     @VisibleForTesting
garbageCollect()235     void garbageCollect() {
236         long elapsedTimeMillis = SystemClock.elapsedRealtime();
237         ArrayList<String> expiredKeys = new ArrayList<>();
238 
239         Iterator<Map.Entry<String, ReauthInfo>> iter = sReauthInfoMap.entrySet().iterator();
240         while (iter.hasNext()) {
241             Map.Entry<String, ReauthInfo> entry = iter.next();
242             if (!entry.getValue().isValid()) {
243                 iter.remove();
244             }
245         }
246     }
247 
248     /**
249      * Retrieves the number of ReauthInfos stored in ReauthInfoMap. Only test purpose.
250      *
251      * @return int : Number of ReauthInfos
252      */
253     @VisibleForTesting
getNumberOfReauthInfo()254     int getNumberOfReauthInfo() {
255         return sReauthInfoMap.size();
256     }
257 
258     /** Clear this ReauthInfoMap. Only test purpose. */
259     @VisibleForTesting
clearReauthInfoMap()260     void clearReauthInfoMap() {
261         sReauthInfoMap.clear();
262     }
263 }
264