mirror of https://github.com/M66B/FairEmail.git
VirusTotal: proof of concept upload
This commit is contained in:
parent
b58f908a51
commit
1d19403a3f
|
@ -27,7 +27,11 @@ import java.io.File;
|
||||||
public class VirusTotal {
|
public class VirusTotal {
|
||||||
static final String URI_PRIVACY = "";
|
static final String URI_PRIVACY = "";
|
||||||
|
|
||||||
static Bundle scan(Context context, File file) {
|
static Bundle lookup(Context context, File file, String apiKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Bundle upload(Context context, File file, String apiKey) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,10 @@ package eu.faircode.email;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
@ -36,8 +33,12 @@ import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
|
||||||
|
@ -46,8 +47,10 @@ public class VirusTotal {
|
||||||
static final String URI_PRIVACY = "https://support.virustotal.com/hc/en-us/articles/115002168385-Privacy-Policy";
|
static final String URI_PRIVACY = "https://support.virustotal.com/hc/en-us/articles/115002168385-Privacy-Policy";
|
||||||
|
|
||||||
private static final int VT_TIMEOUT = 20; // seconds
|
private static final int VT_TIMEOUT = 20; // seconds
|
||||||
|
private static final long VT_ANALYSIS_WAIT = 6000L; // milliseconds
|
||||||
|
private static final int VT_ANALYSIS_CHECKS = 50; // 50 x 6 sec = 5 minutes
|
||||||
|
|
||||||
static Bundle scan(Context context, File file) throws NoSuchAlgorithmException, IOException, JSONException {
|
static Bundle lookup(Context context, File file, String apiKey) throws NoSuchAlgorithmException, IOException, JSONException {
|
||||||
String hash;
|
String hash;
|
||||||
try (InputStream is = new FileInputStream(file)) {
|
try (InputStream is = new FileInputStream(file)) {
|
||||||
hash = Helper.getHash(is, "SHA-256");
|
hash = Helper.getHash(is, "SHA-256");
|
||||||
|
@ -59,11 +62,10 @@ public class VirusTotal {
|
||||||
Bundle result = new Bundle();
|
Bundle result = new Bundle();
|
||||||
result.putString("uri", uri);
|
result.putString("uri", uri);
|
||||||
|
|
||||||
Pair<Integer, String> response = call(context, "api/v3/files/" + hash);
|
if (TextUtils.isEmpty(apiKey))
|
||||||
|
|
||||||
if (response == null)
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
|
Pair<Integer, String> response = call(context, "api/v3/files/" + hash, apiKey);
|
||||||
if (response.first != HttpsURLConnection.HTTP_OK &&
|
if (response.first != HttpsURLConnection.HTTP_OK &&
|
||||||
response.first != HttpsURLConnection.HTTP_NOT_FOUND)
|
response.first != HttpsURLConnection.HTTP_NOT_FOUND)
|
||||||
throw new FileNotFoundException(response.second);
|
throw new FileNotFoundException(response.second);
|
||||||
|
@ -106,19 +108,111 @@ public class VirusTotal {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Pair<Integer, String> call(Context context, String api) throws IOException {
|
static void upload(Context context, File file, String apiKey) throws IOException, JSONException, InterruptedException, TimeoutException {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
// Get upload URL
|
||||||
String apikey = prefs.getString("vt_apikey", null);
|
Pair<Integer, String> response = call(context, "api/v3/files/upload_url", apiKey);
|
||||||
if (TextUtils.isEmpty(apikey))
|
if (response.first != HttpsURLConnection.HTTP_OK)
|
||||||
return null;
|
throw new FileNotFoundException(response.second);
|
||||||
|
JSONObject jurl = new JSONObject(response.second);
|
||||||
|
String upload_url = jurl.getString("data");
|
||||||
|
|
||||||
|
// Upload file
|
||||||
|
String id;
|
||||||
|
String boundary = "----FairEmail." + System.currentTimeMillis();
|
||||||
|
|
||||||
|
URL url = new URL(upload_url);
|
||||||
|
Log.i("VT upload url=" + url);
|
||||||
|
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("POST");
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
connection.setReadTimeout(VT_TIMEOUT * 1000);
|
||||||
|
connection.setConnectTimeout(VT_TIMEOUT * 1000);
|
||||||
|
ConnectionHelper.setUserAgent(context, connection);
|
||||||
|
connection.setRequestProperty("x-apikey", apiKey);
|
||||||
|
connection.setRequestProperty("Accept", "application/json");
|
||||||
|
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
|
||||||
|
connection.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
OutputStream os = connection.getOutputStream();
|
||||||
|
|
||||||
|
PrintWriter writer = new PrintWriter(new OutputStreamWriter(os));
|
||||||
|
writer
|
||||||
|
.append("--").append(boundary).append("\r\n")
|
||||||
|
.append("Content-Disposition: form-data;")
|
||||||
|
.append(" name=\"file\";")
|
||||||
|
.append(" filename=\"").append(file.getName()).append("\"").append("\r\n")
|
||||||
|
.append("Content-Type: application/octet-stream").append("\r\n")
|
||||||
|
.append("Content-Transfer-Encoding: binary").append("\r\n")
|
||||||
|
.append("\r\n")
|
||||||
|
.flush();
|
||||||
|
|
||||||
|
try (InputStream is = new FileInputStream(file)) {
|
||||||
|
Helper.copy(is, os);
|
||||||
|
}
|
||||||
|
|
||||||
|
os.flush();
|
||||||
|
|
||||||
|
writer
|
||||||
|
.append("\r\n")
|
||||||
|
.append("--").append(boundary).append("--").append("\r\n")
|
||||||
|
.flush();
|
||||||
|
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
int status = connection.getResponseCode();
|
||||||
|
if (status != HttpsURLConnection.HTTP_OK) {
|
||||||
|
String error = "Error " + status + ": " + connection.getResponseMessage();
|
||||||
|
try {
|
||||||
|
InputStream is = connection.getErrorStream();
|
||||||
|
if (is != null)
|
||||||
|
error += "\n" + Helper.readStream(is);
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
Log.w(ex);
|
||||||
|
}
|
||||||
|
Log.w("VT " + error);
|
||||||
|
throw new FileNotFoundException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
String r = Helper.readStream(connection.getInputStream());
|
||||||
|
Log.i("VT response=" + r);
|
||||||
|
JSONObject jfile = new JSONObject(r);
|
||||||
|
JSONObject jdata = jfile.getJSONObject("data");
|
||||||
|
id = jdata.getString("id");
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get analysis result
|
||||||
|
for (int i = 0; i < VT_ANALYSIS_CHECKS; i++) {
|
||||||
|
Pair<Integer, String> analyses = call(context, "api/v3/analyses/" + id, apiKey);
|
||||||
|
if (analyses.first != HttpsURLConnection.HTTP_OK)
|
||||||
|
throw new FileNotFoundException(analyses.second);
|
||||||
|
|
||||||
|
JSONObject janalysis = new JSONObject(analyses.second);
|
||||||
|
JSONObject jdata = janalysis.getJSONObject("data");
|
||||||
|
JSONObject jattributes = jdata.getJSONObject("attributes");
|
||||||
|
String status = jattributes.getString("status");
|
||||||
|
Log.i("VT status=" + status);
|
||||||
|
|
||||||
|
if (!"queued".equals(status))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Thread.sleep(VT_ANALYSIS_WAIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TimeoutException("Analysis");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Pair<Integer, String> call(Context context, String api, String apiKey) throws IOException {
|
||||||
URL url = new URL(URI_ENDPOINT + api);
|
URL url = new URL(URI_ENDPOINT + api);
|
||||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||||
connection.setRequestMethod("GET");
|
connection.setRequestMethod("GET");
|
||||||
connection.setReadTimeout(VT_TIMEOUT * 1000);
|
connection.setReadTimeout(VT_TIMEOUT * 1000);
|
||||||
connection.setConnectTimeout(VT_TIMEOUT * 1000);
|
connection.setConnectTimeout(VT_TIMEOUT * 1000);
|
||||||
ConnectionHelper.setUserAgent(context, connection);
|
ConnectionHelper.setUserAgent(context, connection);
|
||||||
connection.setRequestProperty("x-apikey", apikey);
|
connection.setRequestProperty("x-apikey", apiKey);
|
||||||
connection.setRequestProperty("Accept", "application/json");
|
connection.setRequestProperty("Accept", "application/json");
|
||||||
connection.connect();
|
connection.connect();
|
||||||
|
|
||||||
|
@ -137,7 +231,7 @@ public class VirusTotal {
|
||||||
}
|
}
|
||||||
|
|
||||||
String response = Helper.readStream(connection.getInputStream());
|
String response = Helper.readStream(connection.getInputStream());
|
||||||
Log.i("VT response=" + response);
|
//Log.i("VT response=" + response);
|
||||||
return new Pair<>(status, response);
|
return new Pair<>(status, response);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -316,20 +316,19 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onScan(EntityAttachment attachment) {
|
private void onScan(EntityAttachment attachment) {
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
String apiKey = prefs.getString("vt_apikey", null);
|
||||||
|
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putLong("id", attachment.id);
|
args.putSerializable("file", attachment.getFile(context));
|
||||||
|
args.putString("apiKey", apiKey);
|
||||||
|
|
||||||
new SimpleTask<Bundle>() {
|
new SimpleTask<Bundle>() {
|
||||||
@Override
|
@Override
|
||||||
protected Bundle onExecute(Context context, Bundle args) throws Throwable {
|
protected Bundle onExecute(Context context, Bundle args) throws Throwable {
|
||||||
long id = args.getLong("id");
|
File file = (File) args.getSerializable("file");
|
||||||
|
String apiKey = args.getString("apiKey");
|
||||||
DB db = DB.getInstance(context);
|
return VirusTotal.lookup(context, file, apiKey);
|
||||||
EntityAttachment attachment = db.attachment().getAttachment(id);
|
|
||||||
if (attachment == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return VirusTotal.scan(context, attachment.getFile(context));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -361,7 +360,7 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
|
||||||
tvUnknown.setVisibility(count == 0 ? View.VISIBLE : View.GONE);
|
tvUnknown.setVisibility(count == 0 ? View.VISIBLE : View.GONE);
|
||||||
grpAnalysis.setVisibility(count == 0 ? View.GONE : View.VISIBLE);
|
grpAnalysis.setVisibility(count == 0 ? View.GONE : View.VISIBLE);
|
||||||
|
|
||||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setPositiveButton(R.string.title_info, new DialogInterface.OnClickListener() {
|
.setPositiveButton(R.string.title_info, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -369,8 +368,30 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
|
||||||
Helper.view(context, Uri.parse(uri), true);
|
Helper.view(context, Uri.parse(uri), true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null);
|
||||||
.create();
|
|
||||||
|
if (!TextUtils.isEmpty(apiKey) && BuildConfig.DEBUG)
|
||||||
|
builder.setNeutralButton("Upload", new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialogInterface, int i) {
|
||||||
|
new SimpleTask<Void>() {
|
||||||
|
@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();
|
dialog.show();
|
||||||
|
|
||||||
powner.getLifecycle().addObserver(new LifecycleObserver() {
|
powner.getLifecycle().addObserver(new LifecycleObserver() {
|
||||||
|
|
Loading…
Reference in New Issue