// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.cookies; import android.content.Context; import android.os.AsyncTask; import org.chromium.base.ImportantFileWriterAndroid; import org.chromium.base.Log; import org.chromium.base.ThreadUtils; import org.chromium.base.annotations.CalledByNative; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.content.browser.crypto.CipherFactory; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; /** * Responsible for fetching, (de)serializing, and restoring cookies between the CookieJar and an * encrypted file storage. */ public class CookiesFetcher { /** The default file name for the encrypted cookies storage. */ private static final String DEFAULT_COOKIE_FILE_NAME = "COOKIES.DAT"; /** Used for logging. */ private static final String TAG = "CookiesFetcher"; /** Native-side pointer. */ private final long mNativeCookiesFetcher; private final Context mContext; /** * Creates a new fetcher that can use to fetch cookies from cookie jar * or from a file. * * The lifetime of this object is handled internally. Callers only call * the public static methods which construct a CookiesFetcher object. * It remains alive only during the static call or when it is still * waiting for a callback to be invoked. In the latter case, the native * counter part will hold a strong reference to this Java class so the GC * would not collect it until the callback has been invoked. */ private CookiesFetcher(Context context) { // Native side is responsible for destroying itself under all code paths. mNativeCookiesFetcher = nativeInit(); mContext = context.getApplicationContext(); } /** * Fetches the cookie file's path on demand to prevent IO on the main thread. * * @return Path to the cookie file. */ private static String fetchFileName(Context context) { assert !ThreadUtils.runningOnUiThread(); return context.getFileStreamPath(DEFAULT_COOKIE_FILE_NAME).getAbsolutePath(); } /** * Asynchronously fetches cookies from the incognito profile and saves them to a file. * * @param context Context for accessing the file system. */ public static void persistCookies(Context context) { try { new CookiesFetcher(context).persistCookiesInternal(); } catch (RuntimeException e) { e.printStackTrace(); } } private void persistCookiesInternal() { nativePersistCookies(mNativeCookiesFetcher); } /** * If an incognito profile exists, synchronously fetch cookies from the file specified and * populate the incognito profile with it. Otherwise deletes the file and does not restore the * cookies. * * @param context Context for accessing the file system. */ public static void restoreCookies(Context context) { try { if (deleteCookiesIfNecessary(context)) return; restoreCookiesInternal(context); } catch (RuntimeException e) { e.printStackTrace(); } } private static void restoreCookiesInternal(final Context context) { new AsyncTask<Void, Void, List<CanonicalCookie>>() { @Override protected List<CanonicalCookie> doInBackground(Void... voids) { // Read cookies from disk on a background thread to avoid strict mode violations. List<CanonicalCookie> cookies = new ArrayList<CanonicalCookie>(); DataInputStream in = null; try { Cipher cipher = CipherFactory.getInstance().getCipher(Cipher.DECRYPT_MODE); if (cipher == null) { // Something is wrong. Can't encrypt, don't restore cookies. return cookies; } File fileIn = new File(fetchFileName(context)); if (!fileIn.exists()) return cookies; // Nothing to read FileInputStream streamIn = new FileInputStream(fileIn); in = new DataInputStream(new CipherInputStream(streamIn, cipher)); cookies = CanonicalCookie.readListFromStream(in); // The Cookie File should not be restored again. It'll be overwritten // on the next onPause. scheduleDeleteCookiesFile(context); } catch (IOException e) { Log.w(TAG, "IOException during Cookie Restore", e); } catch (Throwable t) { Log.w(TAG, "Error restoring cookies.", t); } finally { try { if (in != null) in.close(); } catch (IOException e) { Log.w(TAG, "IOException during Cooke Restore"); } catch (Throwable t) { Log.w(TAG, "Error restoring cookies.", t); } } return cookies; } @Override protected void onPostExecute(List<CanonicalCookie> cookies) { // We can only access cookies and profiles on the UI thread. for (CanonicalCookie cookie : cookies) { nativeRestoreCookies(cookie.getName(), cookie.getValue(), cookie.getDomain(), cookie.getPath(), cookie.getCreationDate(), cookie.getExpirationDate(), cookie.getLastAccessDate(), cookie.isSecure(), cookie.isHttpOnly(), cookie.getSameSite(), cookie.getPriority()); } } }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); } /** * Ensure the incognito cookies are deleted when the incognito profile is gone. * * @param context Context for accessing the file system. * @return Whether or not the cookies were deleted. */ public static boolean deleteCookiesIfNecessary(Context context) { try { if (Profile.getLastUsedProfile().hasOffTheRecordProfile()) return false; scheduleDeleteCookiesFile(context); } catch (RuntimeException e) { e.printStackTrace(); return false; } return true; } /** * Delete the cookies file. Called when we detect that all incognito tabs have been closed. */ private static void scheduleDeleteCookiesFile(final Context context) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... voids) { File cookiesFile = new File(fetchFileName(context)); if (cookiesFile.exists()) { if (!cookiesFile.delete()) { Log.e(TAG, "Failed to delete " + cookiesFile.getName()); } } return null; } }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); } @CalledByNative private CanonicalCookie createCookie(String name, String value, String domain, String path, long creation, long expiration, long lastAccess, boolean secure, boolean httpOnly, int sameSite, int priority) { return new CanonicalCookie(name, value, domain, path, creation, expiration, lastAccess, secure, httpOnly, sameSite, priority); } @CalledByNative private void onCookieFetchFinished(final CanonicalCookie[] cookies) { // Cookies fetching requires operations with the profile and must be // done in the main thread. Once that is done, do the save to disk // part in {@link AsyncTask} to avoid strict mode violations. new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... voids) { saveFetchedCookiesToDisk(cookies); return null; } }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); } private void saveFetchedCookiesToDisk(CanonicalCookie[] cookies) { DataOutputStream out = null; try { Cipher cipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE); if (cipher == null) { // Something is wrong. Can't encrypt, don't save cookies. return; } ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); CipherOutputStream cipherOut = new CipherOutputStream(byteOut, cipher); out = new DataOutputStream(cipherOut); CanonicalCookie.saveListToStream(out, cookies); out.close(); ImportantFileWriterAndroid.writeFileAtomically( fetchFileName(mContext), byteOut.toByteArray()); out = null; } catch (IOException e) { Log.w(TAG, "IOException during Cookie Fetch"); } catch (Throwable t) { Log.w(TAG, "Error storing cookies.", t); } finally { try { if (out != null) out.close(); } catch (IOException e) { Log.w(TAG, "IOException during Cookie Fetch"); } } } @CalledByNative private CanonicalCookie[] createCookiesArray(int size) { return new CanonicalCookie[size]; } private native long nativeInit(); private native void nativePersistCookies(long nativeCookiesFetcher); private static native void nativeRestoreCookies(String name, String value, String domain, String path, long creation, long expiration, long lastAccess, boolean secure, boolean httpOnly, int sameSite, int priority); }