From 6bcea2f2bcc5eef179ae88d9815981aba781c74a Mon Sep 17 00:00:00 2001 From: Sam Steele Date: Mon, 19 Dec 2022 05:43:24 -0500 Subject: [PATCH] add some additional debug logging around logging out and migrate session cookies to hardware-backed secure storage on newer versions of Android --- build.gradle | 5 +- irccloud-android.iml | 2 +- .../irccloud/android/IRCCloudApplication.java | 7 -- .../android/MarkAsReadBroadcastReceiver.java | 2 +- .../irccloud/android/NetworkConnection.java | 115 ++++++++++++++++-- .../irccloud/android/RemoteInputService.java | 2 +- .../android/activity/BaseActivity.java | 4 +- .../activity/EditConnectionActivity.java | 2 + .../android/activity/LoginActivity.java | 25 ++-- .../activity/PastebinViewerActivity.java | 1 - .../android/activity/PreferencesActivity.java | 3 +- .../android/activity/SAMLAuthActivity.java | 4 +- .../activity/ShareChooserActivity.java | 2 +- 13 files changed, 126 insertions(+), 48 deletions(-) diff --git a/build.gradle b/build.gradle index 9f84faa2f..4f00bceae 100644 --- a/build.gradle +++ b/build.gradle @@ -65,11 +65,11 @@ def getRevision = { -> } android { - compileSdkVersion 32 + compileSdkVersion 33 testBuildType "mockdata" defaultConfig { - versionCode 339 + versionCode 340 versionName "4.29" minSdkVersion 22 targetSdkVersion 31 @@ -359,6 +359,7 @@ dependencies { implementation "androidx.browser:browser:1.4.0" implementation "androidx.exifinterface:exifinterface:1.3.5" implementation "androidx.room:room-runtime:2.4.3" + implementation "androidx.security:security-crypto:1.1.0-alpha04" annotationProcessor "androidx.room:room-compiler:2.4.3" implementation "com.google.android.gms:play-services-base:18.1.0" implementation "com.google.android.gms:play-services-auth:20.3.0" diff --git a/irccloud-android.iml b/irccloud-android.iml index 32c98ee8d..93b1f8751 100644 --- a/irccloud-android.iml +++ b/irccloud-android.iml @@ -80,7 +80,7 @@ - + \ No newline at end of file diff --git a/src/com/irccloud/android/IRCCloudApplication.java b/src/com/irccloud/android/IRCCloudApplication.java index de90ade03..70cc1fa3e 100644 --- a/src/com/irccloud/android/IRCCloudApplication.java +++ b/src/com/irccloud/android/IRCCloudApplication.java @@ -114,13 +114,6 @@ public synchronized void onReceive(Context context, Intent intent) { conn = NetworkConnection.getInstance(); ColorFormatter.init(); - prefs = getSharedPreferences("prefs", 0); - NetworkConnection.IRCCLOUD_HOST = prefs.getString("host", BuildConfig.HOST); - NetworkConnection.IRCCLOUD_PATH = prefs.getString("path", "/"); - - if(!NetworkConnection.IRCCLOUD_HOST.endsWith(".irccloud.com")) - NetworkConnection.IRCCLOUD_HOST = BuildConfig.HOST; - /*try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (0 != (getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE)) diff --git a/src/com/irccloud/android/MarkAsReadBroadcastReceiver.java b/src/com/irccloud/android/MarkAsReadBroadcastReceiver.java index 00a3ff9b9..6031afd07 100644 --- a/src/com/irccloud/android/MarkAsReadBroadcastReceiver.java +++ b/src/com/irccloud/android/MarkAsReadBroadcastReceiver.java @@ -47,7 +47,7 @@ public void onReceive(Context ctx, Intent i) { } } IRCCloudLog.Log(Log.INFO, "IRCCloud", "Mark as read bid" + bid); - NetworkConnection.getInstance().postHeartbeat(cid, bid, highestEid, IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("session_key", "")); + NetworkConnection.getInstance().postHeartbeat(cid, bid, highestEid, NetworkConnection.getInstance().session); IRCCloudLog.Log(Log.INFO, "IRCCloud", "Cancel bid" + bid); NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel(bid); } diff --git a/src/com/irccloud/android/NetworkConnection.java b/src/com/irccloud/android/NetworkConnection.java index 57249ab4f..ca8953dc7 100644 --- a/src/com/irccloud/android/NetworkConnection.java +++ b/src/com/irccloud/android/NetworkConnection.java @@ -38,12 +38,15 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; +import android.util.SparseArray; import android.view.WindowManager; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.widget.Toast; import androidx.core.content.FileProvider; +import androidx.security.crypto.EncryptedSharedPreferences; +import androidx.security.crypto.MasterKey; import com.codebutler.android_websockets.WebSocketClient; import com.datatheorem.android.trustkit.TrustKit; @@ -91,6 +94,7 @@ import java.net.URL; import java.net.URLEncoder; import java.net.UnknownHostException; +import java.security.GeneralSecurityException; import java.text.Normalizer; import java.util.ArrayList; import java.util.Collections; @@ -254,6 +258,7 @@ public interface IRCResultCallback { private HashMap oobTasks = new HashMap(); private ArrayList pendingEdits = new ArrayList<>(); + private final SparseArray reqids = new SparseArray<>(); public synchronized static NetworkConnection getInstance() { if (instance == null) { @@ -336,7 +341,36 @@ public void onReceive(Context context, Intent intent) { @SuppressWarnings("deprecation") public NetworkConnection() { - session = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("session_key", ""); + SharedPreferences prefs = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0); + session = prefs.getString("session_key", ""); + IRCCloudLog.Log(Log.INFO, "IRCCloud", "Session: " + session); + if(session.length() > 0) { + IRCCloudLog.Log(Log.INFO, "IRCCloud", "Migrating session key"); + NetworkConnection.IRCCLOUD_HOST = prefs.getString("host", BuildConfig.HOST); + NetworkConnection.IRCCLOUD_PATH = prefs.getString("path", "/"); + + if(!NetworkConnection.IRCCLOUD_HOST.endsWith(".irccloud.com")) + NetworkConnection.IRCCLOUD_HOST = BuildConfig.HOST; + + set_api_host(NetworkConnection.IRCCLOUD_HOST); + set_api_path(NetworkConnection.IRCCLOUD_PATH); + set_session(session); + + SharedPreferences.Editor editor = prefs.edit(); + editor.remove("session_key"); + editor.remove("host"); + editor.remove("path"); + editor.apply(); + } else { + try { + prefs = getEncryptedSharedPrefs(); + session = prefs.getString("session", ""); + NetworkConnection.IRCCLOUD_HOST = prefs.getString("host", BuildConfig.HOST); + NetworkConnection.IRCCLOUD_PATH = prefs.getString("path", "/"); + } catch (Exception e) { + IRCCloudLog.LogException(e); + } + } String version; String network_type = null; try { @@ -706,11 +740,38 @@ public static void set_api_host(String host) { if (host.endsWith("/")) host = host.substring(0, host.length() - 1); - SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit(); - editor.putString("host", host); - editor.apply(); - NetworkConnection.IRCCLOUD_HOST = host; - IRCCloudLog.Log(Log.INFO, TAG, "API host: " + NetworkConnection.IRCCLOUD_HOST); + try { + SharedPreferences.Editor editor = getEncryptedSharedPrefs().edit(); + editor.putString("host", host); + editor.apply(); + NetworkConnection.IRCCLOUD_HOST = host; + IRCCloudLog.Log(Log.INFO, TAG, "API host: " + NetworkConnection.IRCCLOUD_HOST); + } catch (Exception e) { + IRCCloudLog.LogException(e); + } + } + + public static void set_api_path(String path) { + try { + SharedPreferences.Editor editor = getEncryptedSharedPrefs().edit(); + editor.putString("path", path); + editor.apply(); + NetworkConnection.IRCCLOUD_PATH = path; + IRCCloudLog.Log(Log.INFO, TAG, "API path: " + NetworkConnection.IRCCLOUD_PATH); + } catch (Exception e) { + IRCCloudLog.LogException(e); + } + } + + public void set_session(String session) { + try { + SharedPreferences.Editor editor = getEncryptedSharedPrefs().edit(); + editor.putString("session", session); + editor.apply(); + this.session = session; + } catch (Exception e) { + IRCCloudLog.LogException(e); + } } public void set_pastebin_cookie() { @@ -946,6 +1007,20 @@ public boolean hasResult(BaseTransaction> baseTransaction, List 25) + reqids.clear(); return last_reqid; } catch (Exception e) { printStackTraceToCrashlytics(e); @@ -3268,18 +3358,19 @@ public synchronized void parse_object(IRCCloudJSONObject object, boolean backlog if (object.has("success") && !object.getBoolean("success") && object.has("message")) { IRCCloudLog.Log(Log.ERROR, TAG, "Error: " + object); if(object.getString("message").equals("auth")) { + int session_length = session.length(); + String old_host = IRCCLOUD_HOST; + String old_path = IRCCLOUD_PATH; logout(); notifyHandlers(EVENT_AUTH_FAILED, object); + IRCCloudLog.Log("LOGOUT: Auth error: " + object + " method: " + reqids.get(object.getInt("_reqid")) + " host: " + old_host + " path: " + old_path +" session length: " + session_length); } else if(object.getString("message").equals("set_shard")) { disconnect(); ready = false; - SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getSharedPreferences("prefs", 0).edit(); - editor.putString("session_key", object.getString("cookie")); + set_session(object.getString("cookie")); if (object.has("websocket_path")) { - IRCCLOUD_PATH = object.getString("websocket_path"); + set_api_path(object.getString("websocket_path")); } - editor.putString("path", NetworkConnection.IRCCLOUD_PATH); - editor.apply(); if (object.has("api_host")) { set_api_host(object.getString("api_host")); } diff --git a/src/com/irccloud/android/RemoteInputService.java b/src/com/irccloud/android/RemoteInputService.java index f42773681..077ce4b3c 100644 --- a/src/com/irccloud/android/RemoteInputService.java +++ b/src/com/irccloud/android/RemoteInputService.java @@ -39,7 +39,7 @@ public RemoteInputService() { @Override protected void onHandleIntent(Intent intent) { boolean success = false; - String sk = getSharedPreferences("prefs", 0).getString("session_key", ""); + String sk = NetworkConnection.getInstance().session; if (intent != null && sk != null && sk.length() > 0) { if(intent.hasExtra("eid")) NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel((int)(intent.getLongExtra("eid", -1) / 1000)); diff --git a/src/com/irccloud/android/activity/BaseActivity.java b/src/com/irccloud/android/activity/BaseActivity.java index 6448fa7c8..0fcd66fcc 100644 --- a/src/com/irccloud/android/activity/BaseActivity.java +++ b/src/com/irccloud/android/activity/BaseActivity.java @@ -287,8 +287,7 @@ public void onResume() { f.delete(); } new ImageListPruneTask().execute((Void)null); - String session = getSharedPreferences("prefs", 0).getString("session_key", ""); - if (session.length() > 0 || BuildConfig.MOCK_DATA) { + if ((NetworkConnection.getInstance().session != null && NetworkConnection.getInstance().session.length() > 0) || BuildConfig.MOCK_DATA) { if(conn.notifier) { IRCCloudLog.Log(Log.INFO, "IRCCloud", "Upgrading notifier websocket"); conn.upgrade(); @@ -647,6 +646,7 @@ public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); conn.logout(); + IRCCloudLog.Log("LOGOUT: Logout menu item selected"); if(mGoogleApiClient.isConnected()) { Auth.CredentialsApi.disableAutoSignIn(mGoogleApiClient).setResultCallback(new ResultCallback() { @Override diff --git a/src/com/irccloud/android/activity/EditConnectionActivity.java b/src/com/irccloud/android/activity/EditConnectionActivity.java index 27be657fa..0a4fa010a 100644 --- a/src/com/irccloud/android/activity/EditConnectionActivity.java +++ b/src/com/irccloud/android/activity/EditConnectionActivity.java @@ -34,6 +34,7 @@ import com.irccloud.android.ColorScheme; import com.irccloud.android.IRCCloudApplication; import com.irccloud.android.IRCCloudJSONObject; +import com.irccloud.android.IRCCloudLog; import com.irccloud.android.NetworkConnection; import com.irccloud.android.R; import com.irccloud.android.data.collection.ServersList; @@ -83,6 +84,7 @@ public void onCreate(Bundle savedInstanceState) { public void onClick(View v) { if (ServersList.getInstance().count() < 1) { NetworkConnection.getInstance().logout(); + IRCCloudLog.Log("LOGOUT: EditConnectionActivity cancel button pressed"); Intent i = new Intent(EditConnectionActivity.this, LoginActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(i); diff --git a/src/com/irccloud/android/activity/LoginActivity.java b/src/com/irccloud/android/activity/LoginActivity.java index 11a44305e..2ba86cca6 100644 --- a/src/com/irccloud/android/activity/LoginActivity.java +++ b/src/com/irccloud/android/activity/LoginActivity.java @@ -481,9 +481,10 @@ public void onResume() { private void login_or_connect() { if (NetworkConnection.IRCCLOUD_HOST != null && NetworkConnection.IRCCLOUD_HOST.length() > 0 && getIntent() != null && getIntent().getData() != null && getIntent().getData().getPath().endsWith("/access-link")) { NetworkConnection.getInstance().logout(); + IRCCloudLog.Log("LOGOUT: Access Link launched"); new AccessLinkTask().execute("https://" + NetworkConnection.IRCCLOUD_HOST + "/chat/access-link?" + getIntent().getData().getEncodedQuery().replace("&mobile=1", "") + "&format=json"); setIntent(new Intent(this, LoginActivity.class)); - } else if (getSharedPreferences("prefs", 0).getString("session_key","").length() > 0) { + } else if (NetworkConnection.getInstance().session != null && NetworkConnection.getInstance().session.length() > 0) { Intent i = new Intent(LoginActivity.this, MainActivity.class); i.putExtra("nosplash", true); if (getIntent() != null) { @@ -505,12 +506,6 @@ private void login_or_connect() { protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_SAML) { if (resultCode == RESULT_OK) { - SharedPreferences.Editor editor = getSharedPreferences("prefs", 0).edit(); - editor.putString("session_key", NetworkConnection.getInstance().session); - editor.putString("host", NetworkConnection.IRCCLOUD_HOST); - editor.putString("path", NetworkConnection.IRCCLOUD_PATH); - editor.apply(); - final Intent i = new Intent(LoginActivity.this, MainActivity.class); i.putExtra("nosplash", true); i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); @@ -620,14 +615,10 @@ protected JSONObject doInBackground(Void... arg0) { public void onPostExecute(JSONObject result) { if (result != null && result.has("session")) { try { - NetworkConnection.getInstance().session = result.getString("session"); - SharedPreferences.Editor editor = getSharedPreferences("prefs", 0).edit(); - editor.putString("session_key", result.getString("session")); + NetworkConnection.getInstance().set_session(result.getString("session")); if (result.has("websocket_path")) { - NetworkConnection.IRCCLOUD_PATH = result.getString("websocket_path"); + NetworkConnection.set_api_path(result.getString("websocket_path")); } - editor.putString("path", NetworkConnection.IRCCLOUD_PATH); - editor.apply(); if (result.has("api_host")) { NetworkConnection.set_api_host(result.getString("api_host")); @@ -808,13 +799,10 @@ protected JSONObject doInBackground(String... arg0) { public void onPostExecute(JSONObject result) { if (result != null && result.has("session")) { try { - SharedPreferences.Editor editor = getSharedPreferences("prefs", 0).edit(); - editor.putString("session_key", result.getString("session")); + NetworkConnection.getInstance().set_session(result.getString("session")); if (result.has("websocket_path")) { - NetworkConnection.IRCCLOUD_PATH = result.getString("websocket_path"); + NetworkConnection.set_api_path(result.getString("websocket_path")); } - editor.putString("path", NetworkConnection.IRCCLOUD_PATH); - editor.apply(); if (result.has("api_host")) { NetworkConnection.set_api_host(result.getString("api_host")); @@ -997,6 +985,7 @@ public void onPostExecute(JSONObject result) { if (NetworkConnection.IRCCLOUD_HOST != null && NetworkConnection.IRCCLOUD_HOST.length() > 0 && getIntent() != null && getIntent().getData() != null && getIntent().getData().getPath().endsWith("/access-link")) { NetworkConnection.getInstance().logout(); + IRCCloudLog.Log("LOGOUT: Access Link launched"); new AccessLinkTask().execute("https://" + NetworkConnection.IRCCLOUD_HOST + "/chat/access-link?" + getIntent().getData().getEncodedQuery().replace("&mobile=1", "") + "&format=json"); setIntent(new Intent(LoginActivity.this, LoginActivity.class)); } else { diff --git a/src/com/irccloud/android/activity/PastebinViewerActivity.java b/src/com/irccloud/android/activity/PastebinViewerActivity.java index cda4b751c..e686779a8 100644 --- a/src/com/irccloud/android/activity/PastebinViewerActivity.java +++ b/src/com/irccloud/android/activity/PastebinViewerActivity.java @@ -195,7 +195,6 @@ public void onClick(View view) { mWebView.addJavascriptInterface(new JSInterface(), "Android"); mWebView.getSettings().setLoadWithOverviewMode(false); mWebView.getSettings().setUseWideViewPort(false); - mWebView.getSettings().setAppCacheEnabled(false); mWebView.getSettings().setAllowFileAccess(false); mWebView.setWebViewClient(new WebViewClient() { @Override diff --git a/src/com/irccloud/android/activity/PreferencesActivity.java b/src/com/irccloud/android/activity/PreferencesActivity.java index ea01b42c8..e7b1fad17 100644 --- a/src/com/irccloud/android/activity/PreferencesActivity.java +++ b/src/com/irccloud/android/activity/PreferencesActivity.java @@ -418,7 +418,7 @@ public void onResume() { super.onResume(); IRCCloudApplication.getInstance().onResume(this); - String session = getSharedPreferences("prefs", 0).getString("session_key", ""); + String session = NetworkConnection.getInstance().session; if ((session != null && session.length() > 0) || BuildConfig.MOCK_DATA) { if (conn.getUserInfo() != null) findPreference("name").setSummary(conn.getUserInfo().name); @@ -1390,6 +1390,7 @@ public void onClick(DialogInterface dialog, int which) { public void onIRCResult(IRCCloudJSONObject result) { if(result.getBoolean("success")) { conn.logout(); + IRCCloudLog.Log("LOGOUT: Account deleted"); if (mGoogleApiClient.isConnected() && conn.getUserInfo() != null) { Credential.Builder builder = new Credential.Builder(conn.getUserInfo().email); if (conn.getUserInfo().name != null && conn.getUserInfo().name.length() > 0) diff --git a/src/com/irccloud/android/activity/SAMLAuthActivity.java b/src/com/irccloud/android/activity/SAMLAuthActivity.java index 8240fd4b4..b1e55f420 100644 --- a/src/com/irccloud/android/activity/SAMLAuthActivity.java +++ b/src/com/irccloud/android/activity/SAMLAuthActivity.java @@ -92,7 +92,9 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { for (String c : cookies) { c = c.trim(); if(c.startsWith("session=") && c.length() > 8) { - NetworkConnection.getInstance().session = c.substring(8); + NetworkConnection.getInstance().set_session(c.substring(8)); + NetworkConnection.set_api_host(NetworkConnection.IRCCLOUD_HOST); + NetworkConnection.set_api_path(NetworkConnection.IRCCLOUD_PATH); setResult(RESULT_OK); break; } diff --git a/src/com/irccloud/android/activity/ShareChooserActivity.java b/src/com/irccloud/android/activity/ShareChooserActivity.java index da30d03c3..6512fa3f4 100644 --- a/src/com/irccloud/android/activity/ShareChooserActivity.java +++ b/src/com/irccloud/android/activity/ShareChooserActivity.java @@ -103,7 +103,7 @@ protected void onPause() { protected void onResume() { super.onResume(); IRCCloudApplication.getInstance().onResume(this); - String session = getSharedPreferences("prefs", 0).getString("session_key", ""); + String session = NetworkConnection.getInstance().session; if (session.length() > 0) { if (conn.getState() == NetworkConnection.STATE_DISCONNECTED || conn.getState() == NetworkConnection.STATE_DISCONNECTING) { conn.connect();