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}