package app.attestation.auditor; import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; import android.os.AsyncTask; import android.os.Build; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.security.keystore.StrongBoxUnavailableException; import android.system.Os; import android.system.StructUtsname; import android.util.Log; import com.google.common.io.BaseEncoding; import com.google.common.io.ByteStreams; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.cert.Certificate; import java.security.spec.ECGenParameterSpec; import java.util.Enumeration; import java.util.Properties; import static android.security.keystore.KeyProperties.KEY_ALGORITHM_EC; @TargetApi(26) public class SubmitSampleJob extends JobService { private static final String TAG = "SubmitSampleJob"; private static final int JOB_ID = 2; private static final String SUBMIT_URL = "https://" + RemoteVerifyJob.DOMAIN + "/submit"; private static final int CONNECT_TIMEOUT = 60000; private static final int READ_TIMEOUT = 60000; private static final int NOTIFICATION_ID = 2; private static final String NOTIFICATION_CHANNEL_ID = "sample_submission"; private static final String KEYSTORE_ALIAS_SAMPLE = "sample_attestation_key"; private SubmitTask task; static boolean isScheduled(final Context context) { return context.getSystemService(JobScheduler.class).getPendingJob(JOB_ID) != null; } static void schedule(final Context context) { final ComponentName serviceName = new ComponentName(context, SubmitSampleJob.class); final JobScheduler scheduler = context.getSystemService(JobScheduler.class); if (scheduler.schedule(new JobInfo.Builder(JOB_ID, serviceName) .setPersisted(true) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) .build()) == JobScheduler.RESULT_FAILURE) { throw new RuntimeException("job schedule failed"); } } private class SubmitTask extends AsyncTask<Void, Void, Boolean> { final JobParameters params; SubmitTask(final JobParameters params) { this.params = params; } @Override protected void onPostExecute(final Boolean success) { jobFinished(params, success); } @Override protected Boolean doInBackground(final Void... params) { HttpURLConnection connection = null; try { connection = (HttpURLConnection) new URL(SUBMIT_URL).openConnection(); connection.setConnectTimeout(CONNECT_TIMEOUT); connection.setReadTimeout(READ_TIMEOUT); connection.setDoOutput(true); final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); keyStore.deleteEntry(KEYSTORE_ALIAS_SAMPLE); final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEYSTORE_ALIAS_SAMPLE, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) .setAlgorithmParameterSpec(new ECGenParameterSpec(AttestationProtocol.EC_CURVE)) .setDigests(AttestationProtocol.KEY_DIGEST) .setAttestationChallenge("sample".getBytes()); AttestationProtocol.generateKeyPair(KEY_ALGORITHM_EC, builder.build()); final Certificate[] certs = keyStore.getCertificateChain(KEYSTORE_ALIAS_SAMPLE); keyStore.deleteEntry(KEYSTORE_ALIAS_SAMPLE); Certificate[] strongBoxCerts = null; if (Build.VERSION.SDK_INT >= 28) { try { builder.setIsStrongBoxBacked(true); AttestationProtocol.generateKeyPair(KEY_ALGORITHM_EC, builder.build()); strongBoxCerts = keyStore.getCertificateChain(KEYSTORE_ALIAS_SAMPLE); keyStore.deleteEntry(KEYSTORE_ALIAS_SAMPLE); } catch (final StrongBoxUnavailableException ignored) { } catch (final IOException e) { if (!(e.getCause() instanceof StrongBoxUnavailableException)) { throw e; } } } final Process process = new ProcessBuilder("getprop").start(); try (final InputStream propertyStream = process.getInputStream(); final OutputStream output = connection.getOutputStream()) { for (final Certificate cert : certs) { output.write(BaseEncoding.base64().encode(cert.getEncoded()).getBytes()); output.write("\n".getBytes()); } if (strongBoxCerts != null) { output.write("StrongBox\n".getBytes()); for (final Certificate cert : strongBoxCerts) { output.write(BaseEncoding.base64().encode(cert.getEncoded()).getBytes()); output.write("\n".getBytes()); } } ByteStreams.copy(propertyStream, output); final StructUtsname utsname = Os.uname(); output.write(utsname.toString().getBytes()); output.write("\n".getBytes()); final Properties javaProps = System.getProperties(); final Enumeration<?> javaPropNames = javaProps.propertyNames(); while (javaPropNames.hasMoreElements()) { final String name = (String) javaPropNames.nextElement(); final String value = javaProps.getProperty(name); output.write(name.getBytes()); output.write("=".getBytes()); output.write(value.getBytes()); output.write("\n".getBytes()); } } final int responseCode = connection.getResponseCode(); if (responseCode != 200) { throw new IOException("response code: " + responseCode); } } catch (final GeneralSecurityException | IOException e) { Log.e(TAG, "submit failure", e); return true; } finally { if (connection != null) { connection.disconnect(); } } final Context context = SubmitSampleJob.this; final NotificationManager manager = context.getSystemService(NotificationManager.class); final NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, context.getString(R.string.sample_submission_notification_channel), NotificationManager.IMPORTANCE_LOW); manager.createNotificationChannel(channel); manager.notify(NOTIFICATION_ID, new Notification.Builder(context, NOTIFICATION_CHANNEL_ID) .setContentTitle(context.getString(R.string.sample_submission_notification_title)) .setContentText(context.getString(R.string.sample_submission_notification_content)) .setShowWhen(true) .setSmallIcon(R.drawable.baseline_cloud_upload_white_24) .build()); return false; } } @Override public boolean onStartJob(final JobParameters params) { task = new SubmitTask(params); task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); return true; } @Override public boolean onStopJob(final JobParameters params) { task.cancel(true); return true; } }