From 3606401af813dd7bc465c4298d03b7a633a53619 Mon Sep 17 00:00:00 2001 From: M66B Date: Sun, 24 Jul 2022 21:17:10 +0200 Subject: [PATCH] Added VirusTotal upload --- PRIVACY.md | 1 + .../java/eu/faircode/email/VirusTotal.java | 6 +- .../java/eu/faircode/email/VirusTotal.java | 44 ++-- .../eu/faircode/email/AdapterAttachment.java | 197 ++++++++++++------ .../res/drawable/twotone_file_upload_24.xml | 18 ++ .../main/res/layout/dialog_virus_total.xml | 44 ++++ app/src/main/res/values/strings.xml | 2 + 7 files changed, 225 insertions(+), 87 deletions(-) create mode 100644 app/src/main/res/drawable/twotone_file_upload_24.xml diff --git a/PRIVACY.md b/PRIVACY.md index bbe41ce070..aa62825e0a 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -70,6 +70,7 @@ This table provides a complete overview of all shared data and the conditions un | DeepL | Received or entered message text and target language code | If translating is enabled, upon pressing a translate button | | LanguageTool | Entered message texts | If LanguageTools is enabled, upon long pressing the save draft button | | VirusTotal | [SHA-256 hash](https://en.wikipedia.org/wiki/SHA-2) of attachments | If VirusTotal is enabled, upon long pressing a scan button (*) | +| VirusTotal | Attached file contents | If VirusTotal is enabled, upon long pressing an upload button (*) | | Gravatar | [MD5 hash](https://en.wikipedia.org/wiki/MD5) of email addresses | If Gravatars are enabled, upon receiving a message (*) | | Libravatar | [MD5 hash](https://en.wikipedia.org/wiki/MD5) of email addresses | If Libravatars are enabled, upon receiving a message (*) | | GitHub | None, but see the remarks below | Upon downloading Disconnect's Tracker Protection lists | diff --git a/app/src/dummy/java/eu/faircode/email/VirusTotal.java b/app/src/dummy/java/eu/faircode/email/VirusTotal.java index e261cc3179..b375912a80 100644 --- a/app/src/dummy/java/eu/faircode/email/VirusTotal.java +++ b/app/src/dummy/java/eu/faircode/email/VirusTotal.java @@ -27,11 +27,15 @@ import java.io.File; public class VirusTotal { static final String URI_PRIVACY = ""; + static String getUrl(File file) { + return null; + } + static Bundle lookup(Context context, File file, String apiKey) { return null; } - static Bundle upload(Context context, File file, String apiKey) { + static Bundle upload(Context context, File file, String apiKey, Runnable analyzing) { return null; } } diff --git a/app/src/extra/java/eu/faircode/email/VirusTotal.java b/app/src/extra/java/eu/faircode/email/VirusTotal.java index f0cb0d5aae..adaafd8f5b 100644 --- a/app/src/extra/java/eu/faircode/email/VirusTotal.java +++ b/app/src/extra/java/eu/faircode/email/VirusTotal.java @@ -21,7 +21,6 @@ package eu.faircode.email; import android.content.Context; import android.os.Bundle; -import android.text.TextUtils; import android.util.Pair; import org.json.JSONArray; @@ -50,30 +49,20 @@ public class VirusTotal { private static final long VT_ANALYSIS_WAIT = 6000L; // milliseconds private static final int VT_ANALYSIS_CHECKS = 50; // 50 x 6 sec = 5 minutes + static String getUrl(File file) throws IOException, NoSuchAlgorithmException { + return URI_ENDPOINT + "gui/file/" + getHash(file); + } + static Bundle lookup(Context context, File file, String apiKey) throws NoSuchAlgorithmException, IOException, JSONException { - String hash; - try (InputStream is = new FileInputStream(file)) { - hash = Helper.getHash(is, "SHA-256"); - } - - String uri = URI_ENDPOINT + "gui/file/" + hash; - Log.i("VT uri=" + uri); - Bundle result = new Bundle(); - result.putString("uri", uri); - - if (TextUtils.isEmpty(apiKey)) - return result; - - Pair response = call(context, "api/v3/files/" + hash, apiKey); - if (response.first != HttpsURLConnection.HTTP_OK && - response.first != HttpsURLConnection.HTTP_NOT_FOUND) - throw new FileNotFoundException(response.second); + Pair response = call(context, "api/v3/files/" + getHash(file), apiKey); if (response.first == HttpsURLConnection.HTTP_NOT_FOUND) { result.putInt("count", 0); result.putInt("malicious", 0); - } else { + } else if (response.first != HttpsURLConnection.HTTP_OK) + throw new FileNotFoundException(response.second); + else { // https://developers.virustotal.com/reference/files // Example: https://gist.github.com/M66B/4ea95fdb93fb10bf4047761fcc9ec21a JSONObject jroot = new JSONObject(response.second); @@ -91,14 +80,14 @@ public class VirusTotal { String name = jnames.getString(i); JSONObject jresult = janalysis.getJSONObject(name); String category = jresult.getString("category"); - Log.i("VT " + name + "=" + category); + //Log.i("VT " + name + "=" + category); if (!"type-unsupported".equals(category)) count++; if ("malicious".equals(category)) malicious++; } - Log.i("VT analysis=" + malicious + "/" + count + " label=" + label); + Log.i("VT lookup=" + malicious + "/" + count + " label=" + label); result.putInt("count", count); result.putInt("malicious", malicious); @@ -108,7 +97,8 @@ public class VirusTotal { return result; } - static void upload(Context context, File file, String apiKey) throws IOException, JSONException, InterruptedException, TimeoutException { + static void upload(Context context, File file, String apiKey, Runnable analyzing) + throws IOException, JSONException, InterruptedException, TimeoutException { // Get upload URL Pair response = call(context, "api/v3/files/upload_url", apiKey); if (response.first != HttpsURLConnection.HTTP_OK) @@ -186,6 +176,8 @@ public class VirusTotal { // Get analysis result for (int i = 0; i < VT_ANALYSIS_CHECKS; i++) { + analyzing.run(); + Pair analyses = call(context, "api/v3/analyses/" + id, apiKey); if (analyses.first != HttpsURLConnection.HTTP_OK) throw new FileNotFoundException(analyses.second); @@ -205,7 +197,13 @@ public class VirusTotal { throw new TimeoutException("Analysis"); } - static Pair call(Context context, String api, String apiKey) throws IOException { + private static String getHash(File file) throws IOException, NoSuchAlgorithmException { + try (InputStream is = new FileInputStream(file)) { + return Helper.getHash(is, "SHA-256"); + } + } + + private static Pair call(Context context, String api, String apiKey) throws IOException { URL url = new URL(URI_ENDPOINT + api); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); connection.setRequestMethod("GET"); diff --git a/app/src/main/java/eu/faircode/email/AdapterAttachment.java b/app/src/main/java/eu/faircode/email/AdapterAttachment.java index 7469055757..7ca710253f 100644 --- a/app/src/main/java/eu/faircode/email/AdapterAttachment.java +++ b/app/src/main/java/eu/faircode/email/AdapterAttachment.java @@ -30,6 +30,7 @@ import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ProgressBar; @@ -319,95 +320,165 @@ public class AdapterAttachment extends RecyclerView.Adapter taskLookup = new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + pbWait.setVisibility(View.VISIBLE); + } + + @Override + protected void onPostExecute(Bundle args) { + pbWait.setVisibility(View.GONE); + } - new SimpleTask() { @Override protected Bundle onExecute(Context context, Bundle args) throws Throwable { - File file = (File) args.getSerializable("file"); String apiKey = args.getString("apiKey"); + File file = (File) args.getSerializable("file"); return VirusTotal.lookup(context, file, apiKey); } @Override protected void onExecuted(Bundle args, Bundle result) { - String uri = result.getString("uri"); int count = result.getInt("count", -1); int malicious = result.getInt("malicious", -1); String label = result.getString("label"); - if (count < 0) { - Helper.view(context, Uri.parse(uri), true); - return; - } - - View view = LayoutInflater.from(context).inflate(R.layout.dialog_virus_total, null); - final TextView tvName = view.findViewById(R.id.tvName); - final ProgressBar pbAnalysis = view.findViewById(R.id.pbAnalysis); - final TextView tvCount = view.findViewById(R.id.tvCount); - final TextView tvLabel = view.findViewById(R.id.tvLabel); - final TextView tvUnknown = view.findViewById(R.id.tvUnknown); - final Group grpAnalysis = view.findViewById(R.id.grpAnalysis); - - tvName.setText(attachment.name); pbAnalysis.setMax(count); pbAnalysis.setProgress(malicious); tvCount.setText(malicious + "/" + count); tvLabel.setText(label); tvLabel.setVisibility(TextUtils.isEmpty(label) ? View.GONE : View.VISIBLE); tvUnknown.setVisibility(count == 0 ? View.VISIBLE : View.GONE); + btnUpload.setVisibility(count == 0 && !TextUtils.isEmpty(apiKey) ? View.VISIBLE : View.GONE); grpAnalysis.setVisibility(count == 0 ? View.GONE : View.VISIBLE); - - AlertDialog.Builder builder = new AlertDialog.Builder(context) - .setView(view) - .setPositiveButton(R.string.title_info, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - Helper.view(context, Uri.parse(uri), true); - } - }) - .setNegativeButton(android.R.string.cancel, null); - - if (!TextUtils.isEmpty(apiKey) && BuildConfig.DEBUG) - builder.setNeutralButton("Upload", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - new SimpleTask() { - @Override - protected Void onExecute(Context context, Bundle args) throws Throwable { - File file = (File) args.getSerializable("file"); - String apiKey = args.getString("apiKey"); - VirusTotal.upload(context, file, apiKey); - return null; - } - - @Override - protected void onException(Bundle args, Throwable ex) { - Log.unexpectedError(parentFragment.getParentFragmentManager(), ex); - } - }.execute(context, owner, args, "attachment:upload"); - } - }); - - AlertDialog dialog = builder.create(); - dialog.show(); - - powner.getLifecycle().addObserver(new LifecycleObserver() { - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - public void onDestroy() { - dialog.dismiss(); - powner.getLifecycle().removeObserver(this); - } - }); } @Override protected void onException(Bundle args, Throwable ex) { Log.unexpectedError(parentFragment.getParentFragmentManager(), ex); } - }.execute(context, owner, args, "attachment:scan"); + }; + + final SimpleTask taskUpload = new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + btnUpload.setEnabled(false); + pbUpload.setVisibility(View.VISIBLE); + } + + @Override + protected void onPostExecute(Bundle args) { + btnUpload.setEnabled(true); + tvAnalyzing.setVisibility(View.GONE); + pbUpload.setVisibility(View.GONE); + } + + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + String apiKey = args.getString("apiKey"); + File file = (File) args.getSerializable("file"); + VirusTotal.upload(context, file, apiKey, new Runnable() { + private int step = 0; + + @Override + public void run() { + postProgress(Integer.toString(++step)); + } + }); + return null; + } + + @Override + protected void onProgress(CharSequence status, Bundle data) { + tvAnalyzing.setVisibility(View.VISIBLE); + } + + @Override + protected void onExecuted(Bundle args, Void data) { + taskLookup.execute(context, owner, args, "attachment:lookup"); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(parentFragment.getParentFragmentManager(), ex); + } + }; + + final SimpleTask taskUrl = new SimpleTask() { + @Override + protected String onExecute(Context context, Bundle args) throws Throwable { + File file = (File) args.getSerializable("file"); + return VirusTotal.getUrl(file); + } + + @Override + protected void onExecuted(Bundle args, String uri) { + Helper.view(context, Uri.parse(uri), true); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(parentFragment.getParentFragmentManager(), ex); + } + }; + + btnUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + taskUpload.execute(context, owner, args, "attachment:upload"); + } + }); + + if (!TextUtils.isEmpty(apiKey)) + taskLookup.execute(context, owner, args, "attachment:lookup"); + else + pbWait.setVisibility(View.GONE); + + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setView(view) + .setPositiveButton(R.string.title_info, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + taskUrl.execute(context, owner, args, "attachment:virustotal"); + } + }) + .setNegativeButton(android.R.string.cancel, null); + + AlertDialog dialog = builder.create(); + dialog.show(); + + powner.getLifecycle().addObserver(new LifecycleObserver() { + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + public void onDestroy() { + dialog.dismiss(); + powner.getLifecycle().removeObserver(this); + } + }); } private void onShare(EntityAttachment attachment) { diff --git a/app/src/main/res/drawable/twotone_file_upload_24.xml b/app/src/main/res/drawable/twotone_file_upload_24.xml new file mode 100644 index 0000000000..0e6f435d0b --- /dev/null +++ b/app/src/main/res/drawable/twotone_file_upload_24.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/layout/dialog_virus_total.xml b/app/src/main/res/layout/dialog_virus_total.xml index 2a7ac9d783..6a4ef6f306 100644 --- a/app/src/main/res/layout/dialog_virus_total.xml +++ b/app/src/main/res/layout/dialog_virus_total.xml @@ -84,6 +84,50 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvLabel" /> +