001package com.gigya.android.sdk.session; 002 003import android.annotation.SuppressLint; 004import android.content.Context; 005import android.content.Intent; 006import android.os.CountDownTimer; 007import android.support.annotation.Nullable; 008import android.support.v4.content.LocalBroadcastManager; 009import android.support.v4.util.ArrayMap; 010import android.text.TextUtils; 011 012import com.gigya.android.sdk.Config; 013import com.gigya.android.sdk.GigyaDefinitions; 014import com.gigya.android.sdk.GigyaInterceptor; 015import com.gigya.android.sdk.GigyaLogger; 016import com.gigya.android.sdk.encryption.EncryptionException; 017import com.gigya.android.sdk.encryption.ISecureKey; 018import com.gigya.android.sdk.persistence.IPersistenceService; 019import com.gigya.android.sdk.utils.CipherUtils; 020import com.gigya.android.sdk.utils.ObjectUtils; 021import com.google.gson.Gson; 022 023import org.json.JSONObject; 024 025import java.security.Key; 026import java.util.Map; 027import java.util.concurrent.TimeUnit; 028 029import javax.crypto.Cipher; 030import javax.crypto.SecretKey; 031 032public class SessionService implements ISessionService { 033 034 private static final String LOG_TAG = "SessionService"; 035 036 // Final fields. 037 final private Context _context; 038 final private Config _config; 039 final private IPersistenceService _psService; 040 final private ISecureKey _secureKey; 041 042 // Dynamic field - session heap. 043 private SessionInfo _sessionInfo; 044 045 // Injected field - session logic interceptors. 046 private ArrayMap<String, GigyaInterceptor> _sessionInterceptors = new ArrayMap<>(); 047 048 public SessionService(Context context, 049 Config config, 050 IPersistenceService psService, 051 ISecureKey secureKey) { 052 _context = context; 053 _psService = psService; 054 _config = config; 055 _secureKey = secureKey; 056 } 057 058 @SuppressLint("GetInstance") 059 @Nullable 060 @Override 061 public String encryptSession(String plain, Key key) throws EncryptionException { 062 try { 063 final String ENCRYPTION_ALGORITHM = "AES"; 064 final Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); 065 cipher.init(Cipher.ENCRYPT_MODE, key); 066 byte[] byteCipherText = cipher.doFinal(plain.getBytes()); 067 return CipherUtils.bytesToString(byteCipherText); 068 } catch (Exception ex) { 069 ex.printStackTrace(); 070 throw new EncryptionException("encryptSession: exception" + ex.getMessage(), ex.getCause()); 071 } 072 } 073 074 @SuppressLint("GetInstance") 075 @Nullable 076 @Override 077 public String decryptSession(String encrypted, Key key) throws EncryptionException { 078 try { 079 final String ENCRYPTION_ALGORITHM = "AES"; 080 final Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); 081 cipher.init(Cipher.DECRYPT_MODE, key); 082 byte[] encPLBytes = CipherUtils.stringToBytes(encrypted); 083 byte[] bytePlainText = cipher.doFinal(encPLBytes); 084 return new String(bytePlainText); 085 } catch (Exception ex) { 086 ex.printStackTrace(); 087 throw new EncryptionException("decryptSession: exception" + ex.getMessage(), ex.getCause()); 088 } 089 } 090 091 /** 092 * Persist session info using current encryption algorithm. 093 * 094 * @param sessionInfo Provided session. 095 */ 096 @Override 097 public void save(SessionInfo sessionInfo) { 098 final String encryptionType = _psService.getSessionEncryptionType(); 099 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 (valid && _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}