· 6 years ago · Oct 14, 2019, 03:34 PM
1package com.gigya.android.sdk.session;
2
3import android.annotation.SuppressLint;
4import android.content.Context;
5import android.content.Intent;
6import android.os.CountDownTimer;
7import android.support.annotation.Nullable;
8import android.support.v4.content.LocalBroadcastManager;
9import android.support.v4.util.ArrayMap;
10import android.text.TextUtils;
11
12import com.gigya.android.sdk.Config;
13import com.gigya.android.sdk.GigyaDefinitions;
14import com.gigya.android.sdk.GigyaInterceptor;
15import com.gigya.android.sdk.GigyaLogger;
16import com.gigya.android.sdk.encryption.EncryptionException;
17import com.gigya.android.sdk.encryption.ISecureKey;
18import com.gigya.android.sdk.persistence.IPersistenceService;
19import com.gigya.android.sdk.utils.CipherUtils;
20import com.gigya.android.sdk.utils.ObjectUtils;
21import com.google.gson.Gson;
22
23import org.json.JSONObject;
24
25import java.security.Key;
26import java.util.Map;
27import java.util.concurrent.TimeUnit;
28
29import javax.crypto.Cipher;
30import javax.crypto.SecretKey;
31
32public class SessionService implements ISessionService {
33
34 private static final String LOG_TAG = "SessionService";
35
36 // Final fields.
37 final private Context _context;
38 final private Config _config;
39 final private IPersistenceService _psService;
40 final private ISecureKey _secureKey;
41
42 // Dynamic field - session heap.
43 private SessionInfo _sessionInfo;
44
45 // Injected field - session logic interceptors.
46 private ArrayMap<String, GigyaInterceptor> _sessionInterceptors = new ArrayMap<>();
47
48 public SessionService(Context context,
49 Config config,
50 IPersistenceService psService,
51 ISecureKey secureKey) {
52 _context = context;
53 _psService = psService;
54 _config = config;
55 _secureKey = secureKey;
56 }
57
58 @SuppressLint("GetInstance")
59 @Nullable
60 @Override
61 public String encryptSession(String plain, Key key) throws EncryptionException {
62 try {
63 final String ENCRYPTION_ALGORITHM = "AES";
64 final Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
65 cipher.init(Cipher.ENCRYPT_MODE, key);
66 byte[] byteCipherText = cipher.doFinal(plain.getBytes());
67 return CipherUtils.bytesToString(byteCipherText);
68 } catch (Exception ex) {
69 ex.printStackTrace();
70 throw new EncryptionException("encryptSession: exception" + ex.getMessage(), ex.getCause());
71 }
72 }
73
74 @SuppressLint("GetInstance")
75 @Nullable
76 @Override
77 public String decryptSession(String encrypted, Key key) throws EncryptionException {
78 try {
79 final String ENCRYPTION_ALGORITHM = "AES";
80 final Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
81 cipher.init(Cipher.DECRYPT_MODE, key);
82 byte[] encPLBytes = CipherUtils.stringToBytes(encrypted);
83 byte[] bytePlainText = cipher.doFinal(encPLBytes);
84 return new String(bytePlainText);
85 } catch (Exception ex) {
86 ex.printStackTrace();
87 throw new EncryptionException("decryptSession: exception" + ex.getMessage(), ex.getCause());
88 }
89 }
90
91 /**
92 * Persist session info using current encryption algorithm.
93 *
94 * @param sessionInfo Provided session.
95 */
96 @Override
97 public void save(SessionInfo sessionInfo) {
98 final String encryptionType = _psService.getSessionEncryptionType();
99 if (!encryptionType.equals(GigyaDefinitions.SessionEncryption.DEFAULT)) {
100 // Saving & encrypting the session via this service is only viable for "default" session encryption.
101 return;
102 }
103 try {
104 // Update persistence.
105 final JSONObject jsonObject = new JSONObject()
106 .put("sessionToken", sessionInfo == null ? null : sessionInfo.getSessionToken())
107 .put("sessionSecret", sessionInfo == null ? null : sessionInfo.getSessionSecret())
108 .put("expirationTime", sessionInfo == null ? null : sessionInfo.getExpirationTime())
109 .put("ucid", _config.getUcid())
110 .put("gmid", _config.getGmid());
111 final String json = jsonObject.toString();
112 final SecretKey key = _secureKey.getKey();
113 final String encryptedSession = encryptSession(json, key);
114 // Save session.
115 _psService.setSession(encryptedSession);
116 } catch (Exception ex) {
117 ex.printStackTrace();
118 }
119 }
120
121 /**
122 * Load current persistent session.
123 */
124 @Override
125 public void load() {
126 // Check & load legacy session if available.
127 if (isLegacySession()) {
128 GigyaLogger.debug(LOG_TAG, "load: isLegacySession!! Will migrate to update structure");
129 _sessionInfo = loadLegacySession();
130 return;
131 }
132 if (_psService.isSessionAvailable()) {
133 String encryptedSession = _psService.getSession();
134 if (!TextUtils.isEmpty(encryptedSession)) {
135 final String encryptionType = _psService.getSessionEncryptionType();
136 if (ObjectUtils.safeEquals(encryptionType, GigyaDefinitions.SessionEncryption.FINGERPRINT)) {
137 GigyaLogger.debug(LOG_TAG, "Fingerprint session available. Load stops until unlocked");
138 }
139 try {
140 final SecretKey key = _secureKey.getKey();
141 final String decryptedSession = decryptSession(encryptedSession, key);
142 Gson gson = new Gson();
143 // Parse session info.
144 final SessionInfo sessionInfo = gson.fromJson(decryptedSession, SessionInfo.class);
145 // Parse config fields. & update main SDK config instance.
146 final Config dynamicConfig = gson.fromJson(decryptedSession, Config.class);
147 _config.updateWith(dynamicConfig);
148 _sessionInfo = sessionInfo;
149 // Refresh expiration. If any.
150 refreshSessionExpiration();
151 } catch (Exception eex) {
152 eex.printStackTrace();
153 }
154 }
155 }
156 }
157
158 /**
159 * Get current available session.
160 *
161 * @return Current session or null If none exist.
162 */
163 @Override
164 public SessionInfo getSession() {
165 return _sessionInfo;
166 }
167
168 /**
169 * External session setter interface.
170 * Will override the current session with given session info.
171 * Session will be also persist.
172 *
173 * @param sessionInfo Provided session.
174 */
175 @Override
176 public void setSession(SessionInfo sessionInfo) {
177 _sessionInfo = sessionInfo;
178 save(sessionInfo); // Will only work for "DEFAULT" encryption.
179 // Apply interceptions
180 applyInterceptions();
181
182 // Check session expiration.
183 if (_sessionInfo.getExpirationTime() > 0) {
184 _sessionWillExpireIn = System.currentTimeMillis() + (_sessionInfo.getExpirationTime() * 1000);
185 startSessionCountdownTimerIfNeeded();
186 }
187 }
188
189 /**
190 * Check id current session validity.
191 * Validity is evaluated via theses constraints:
192 * #1 - Session object reference is not null.
193 * #2 - Session contains token and secret.
194 * #3 - If session contains expiration, check if not yet expired.
195 *
196 * @return True if session is valid.
197 */
198 @Override
199 public boolean isValid() {
200 boolean valid = _sessionInfo != null && _sessionInfo.isValid();
201 if (_sessionWillExpireIn > 0) {
202 valid = System.currentTimeMillis() < _sessionWillExpireIn;
203 }
204 return valid;
205 }
206
207 /**
208 * Clear session from memory.
209 *
210 * @param clearStorage Set True if session should be cleared from persistence as well.
211 */
212 @Override
213 public void clear(boolean clearStorage) {
214 GigyaLogger.debug(LOG_TAG, "clear: ");
215 _sessionInfo = null;
216
217 if (clearStorage) {
218 // Remove session data. Update encryption to DEFAULT.
219 _psService.removeSession();
220 _psService.setSessionEncryptionType(GigyaDefinitions.SessionEncryption.DEFAULT);
221
222 // Make sure to keep reference to GMID & UCID if available.
223 if (_config.getGmid() != null && _config.getUcid() != null) {
224 try {
225 // Encrypt again & save.
226 final JSONObject jsonObject = new JSONObject().put("ucid", _config.getUcid()).put("gmid", _config.getGmid());
227 final String encryptedSession = encryptSession(jsonObject.toString(), _secureKey.getKey());
228 _psService.setSession(encryptedSession);
229 } catch (Exception e) {
230 e.printStackTrace();
231 }
232 }
233 }
234 }
235
236 private void applyInterceptions() {
237 if (_sessionInterceptors.isEmpty()) {
238 return;
239 }
240 for (Map.Entry<String, GigyaInterceptor> entry : _sessionInterceptors.entrySet()) {
241 final GigyaInterceptor interceptor = entry.getValue();
242 GigyaLogger.debug(LOG_TAG, "Apply interception for: " + interceptor.getName());
243 interceptor.intercept();
244 }
245 }
246
247 //region LEGACY SESSION
248
249 private boolean isLegacySession() {
250 final String legacyTokenKey = "session.Token";
251 return (!TextUtils.isEmpty(_psService.getString(legacyTokenKey, null)));
252 }
253
254 private SessionInfo loadLegacySession() {
255 final String token = _psService.getString("session.Token", null);
256 final String secret = _psService.getString("session.Secret", null);
257 final long expiration = _psService.getLong("session.ExpirationTime", 0L);
258 final SessionInfo sessionInfo = new SessionInfo(secret, token, expiration);
259 // Update configuration fields.
260 final String ucid = _psService.getString("ucid", null);
261 final String gmid = _psService.getString("gmid", null);
262 final Config dynamicConfig = new Config();
263 dynamicConfig.setUcid(ucid);
264 dynamicConfig.setGmid(gmid);
265 _config.updateWith(dynamicConfig);
266 // Clear all legacy session entries.
267 _psService.removeLegacySession();
268 // Save session in current construct.
269 save(sessionInfo);
270 return sessionInfo;
271 }
272
273 //endregion
274
275 //region SESSION EXPIRATION
276
277 private long _sessionWillExpireIn = 0;
278
279 private CountDownTimer _sessionLifeCountdownTimer;
280
281 /**
282 * Cancel running timer if reference is not null.
283 */
284 @Override
285 public void cancelSessionCountdownTimer() {
286 if (_sessionLifeCountdownTimer != null) _sessionLifeCountdownTimer.cancel();
287 }
288
289 /**
290 * Add custom session interception.
291 * Interception will apply when you set a new session using setSession {@link #setSession}
292 *
293 * @param interceptor Provided interceptor implementation.
294 */
295 @Override
296 public void addInterceptor(GigyaInterceptor interceptor) {
297 _sessionInterceptors.put(interceptor.getName(), interceptor);
298 }
299
300 /**
301 * Refresh the current session expiration timestamp.
302 * For internal use.
303 */
304 @Override
305 public void refreshSessionExpiration() {
306 // Get session expiration if exists.
307 _sessionWillExpireIn = _psService.getSessionExpiration();
308 // Check if already passed. Reset if so.
309 if (_sessionWillExpireIn > 0 && _sessionWillExpireIn < System.currentTimeMillis()) {
310 _psService.setSessionExpiration(_sessionWillExpireIn = 0);
311 }
312 }
313
314 /**
315 * Check if session countdown is required. Initiate if needed.
316 */
317 @Override
318 public void startSessionCountdownTimerIfNeeded() {
319 if (_sessionInfo == null) {
320 return;
321 }
322 if (_sessionInfo.isValid() && _sessionWillExpireIn > 0) {
323 // Session is set to expire.
324 final long timeUntilSessionExpires = _sessionWillExpireIn - System.currentTimeMillis();
325 GigyaLogger.debug(LOG_TAG, "startSessionCountdownTimerIfNeeded: Session is set to expire in: "
326 + (timeUntilSessionExpires / 1000) + " start countdown timer");
327 // Just in case.
328 if (timeUntilSessionExpires > 0) {
329 startSessionCountdown(timeUntilSessionExpires);
330 }
331 }
332 }
333
334 /**
335 * Initiate session expiration countdown.
336 * When finished. A local broadcast will be triggered.
337 *
338 * @param future Number of milliseconds to count down.
339 */
340 private void startSessionCountdown(long future) {
341 cancelSessionCountdownTimer();
342 _sessionLifeCountdownTimer = new CountDownTimer(future, TimeUnit.SECONDS.toMillis(1)) {
343 @Override
344 public void onTick(long millisUntilFinished) {
345 // KEEP THIS LOG COMMENTED TO AVOID SPAMMING LOG_CAT!!!!!
346 //GigyaLogger.debug(LOG_TAG, "startSessionCountdown: Seconds remaining until session will expire = " + millisUntilFinished / 1000);
347 }
348
349 @Override
350 public void onFinish() {
351 GigyaLogger.debug(LOG_TAG, "startSessionCountdown: Session expiration countdown done! Session is invalid");
352 _psService.setSessionExpiration(_sessionWillExpireIn = 0);
353 // Clear the session from heap & persistence.
354 clear(true);
355 // Send "session expired" local broadcast.
356 LocalBroadcastManager.getInstance(_context).sendBroadcast(new Intent(GigyaDefinitions.Broadcasts.INTENT_ACTION_SESSION_EXPIRED));
357 }
358 }.start();
359 }
360
361 //endregion
362}