diff options
Diffstat (limited to 'tzdata/update_test_app')
-rw-r--r-- | tzdata/update_test_app/Android.mk | 25 | ||||
-rw-r--r-- | tzdata/update_test_app/AndroidManifest.xml | 35 | ||||
-rw-r--r-- | tzdata/update_test_app/res/drawable/ic_launcher.png | bin | 0 -> 9397 bytes | |||
-rw-r--r-- | tzdata/update_test_app/res/layout/activity_main.xml | 106 | ||||
-rw-r--r-- | tzdata/update_test_app/res/values/strings.xml | 13 | ||||
-rw-r--r-- | tzdata/update_test_app/res/xml/filepaths.xml | 4 | ||||
-rw-r--r-- | tzdata/update_test_app/src/libcore/tzdata/update_test_app/installupdatetestapp/MainActivity.java | 271 |
7 files changed, 454 insertions, 0 deletions
diff --git a/tzdata/update_test_app/Android.mk b/tzdata/update_test_app/Android.mk new file mode 100644 index 0000000..ee70819 --- /dev/null +++ b/tzdata/update_test_app/Android.mk @@ -0,0 +1,25 @@ +# Copyright (C) 2015 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE_TAGS := optional +LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS) +LOCAL_PROGUARD_ENABLED := disabled +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_PACKAGE_NAME := UpdateTestApp +LOCAL_CERTIFICATE := platform +include $(BUILD_PACKAGE) diff --git a/tzdata/update_test_app/AndroidManifest.xml b/tzdata/update_test_app/AndroidManifest.xml new file mode 100644 index 0000000..67a8450 --- /dev/null +++ b/tzdata/update_test_app/AndroidManifest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="libcore.tzdata.update_test_app.installupdatetestapp" > + + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + + <application + android:allowBackup="false" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:theme="@android:style/Theme.Holo.Light"> + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + <provider + android:name="android.support.v4.content.FileProvider" + android:authorities="libcore.tzdata.update_test_app.fileprovider" + android:grantUriPermissions="true" + android:exported="false"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/filepaths" /> + </provider> + + </application> + +</manifest> diff --git a/tzdata/update_test_app/res/drawable/ic_launcher.png b/tzdata/update_test_app/res/drawable/ic_launcher.png Binary files differnew file mode 100644 index 0000000..96a442e --- /dev/null +++ b/tzdata/update_test_app/res/drawable/ic_launcher.png diff --git a/tzdata/update_test_app/res/layout/activity_main.xml b/tzdata/update_test_app/res/layout/activity_main.xml new file mode 100644 index 0000000..b265837 --- /dev/null +++ b/tzdata/update_test_app/res/layout/activity_main.xml @@ -0,0 +1,106 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:context=".MainActivity"> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/action" + android:id="@+id/action_label" /> + + <EditText + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/action" + android:layout_weight="1" + android:text="@string/default_action" /> + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/version" + android:id="@+id/version_label" /> + + <EditText + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/version" + android:layout_weight="1" + android:text="@string/default_version" /> + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/content_path" + android:id="@+id/content_path_label" /> + + <EditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/default_content_path" + android:id="@+id/content_path" /> + + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/required_hash" + android:id="@+id/required_hash_label" /> + + <EditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/default_required_hash" + android:id="@+id/required_hash" /> + + </LinearLayout> + + <Button + android:id="@+id/trigger_install_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:text="@string/trigger_install" /> + + <ScrollView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:scrollbars="vertical" + android:fillViewport="true"> + + <TextView + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:id="@+id/log" + android:singleLine="false" /> + + </ScrollView> + +</LinearLayout> diff --git a/tzdata/update_test_app/res/values/strings.xml b/tzdata/update_test_app/res/values/strings.xml new file mode 100644 index 0000000..524f9d8 --- /dev/null +++ b/tzdata/update_test_app/res/values/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">InstallUpdateTestApp</string> + <string name="action">Action</string> + <string name="content_path">Content Path</string> + <string name="default_action">android.intent.action.UPDATE_TZDATA</string> + <string name="default_content_path">/data/local/tmp/out.zip</string> + <string name="default_required_hash">NONE</string> + <string name="default_version">1</string> + <string name="required_hash">Required Hash</string> + <string name="trigger_install">Trigger Install</string> + <string name="version">Version</string> +</resources> diff --git a/tzdata/update_test_app/res/xml/filepaths.xml b/tzdata/update_test_app/res/xml/filepaths.xml new file mode 100644 index 0000000..c95b8f4 --- /dev/null +++ b/tzdata/update_test_app/res/xml/filepaths.xml @@ -0,0 +1,4 @@ +<!-- Used by FileProvider. See AndroidManifest.xml --> +<paths> + <files-path path="temp/" name="temp" /> +</paths> diff --git a/tzdata/update_test_app/src/libcore/tzdata/update_test_app/installupdatetestapp/MainActivity.java b/tzdata/update_test_app/src/libcore/tzdata/update_test_app/installupdatetestapp/MainActivity.java new file mode 100644 index 0000000..f9d911b --- /dev/null +++ b/tzdata/update_test_app/src/libcore/tzdata/update_test_app/installupdatetestapp/MainActivity.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package libcore.tzdata.update_test_app.installupdatetestapp; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.Settings; +import android.support.v4.content.FileProvider; +import android.util.Base64; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Date; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + + +public class MainActivity extends Activity implements View.OnClickListener { + + private static final String UPDATE_CERTIFICATE_KEY = "config_update_certificate"; + private static final String EXTRA_REQUIRED_HASH = "REQUIRED_HASH"; + private static final String EXTRA_SIGNATURE = "SIGNATURE"; + private static final String EXTRA_VERSION_NUMBER = "VERSION"; + + public static final String TEST_CERT = "" + + "MIIDsjCCAxugAwIBAgIJAPLf2gS0zYGUMA0GCSqGSIb3DQEBBQUAMIGYMQswCQYDVQQGEwJVUzET" + + "MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEPMA0GA1UEChMGR29v" + + "Z2xlMRAwDgYDVQQLEwd0ZXN0aW5nMRYwFAYDVQQDEw1HZXJlbXkgQ29uZHJhMSEwHwYJKoZIhvcN" + + "AQkBFhJnY29uZHJhQGdvb2dsZS5jb20wHhcNMTIwNzE0MTc1MjIxWhcNMTIwODEzMTc1MjIxWjCB" + + "mDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp" + + "ZXcxDzANBgNVBAoTBkdvb2dsZTEQMA4GA1UECxMHdGVzdGluZzEWMBQGA1UEAxMNR2VyZW15IENv" + + "bmRyYTEhMB8GCSqGSIb3DQEJARYSZ2NvbmRyYUBnb29nbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUA" + + "A4GNADCBiQKBgQCjGGHATBYlmas+0sEECkno8LZ1KPglb/mfe6VpCT3GhSr+7br7NG/ZwGZnEhLq" + + "E7YIH4fxltHmQC3Tz+jM1YN+kMaQgRRjo/LBCJdOKaMwUbkVynAH6OYsKevjrOPk8lfM5SFQzJMG" + + "sA9+Tfopr5xg0BwZ1vA/+E3mE7Tr3M2UvwIDAQABo4IBADCB/TAdBgNVHQ4EFgQUhzkS9E6G+x8W" + + "L4EsmRjDxu28tHUwgc0GA1UdIwSBxTCBwoAUhzkS9E6G+x8WL4EsmRjDxu28tHWhgZ6kgZswgZgx" + + "CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3" + + "MQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNVBAsTB3Rlc3RpbmcxFjAUBgNVBAMTDUdlcmVteSBDb25k" + + "cmExITAfBgkqhkiG9w0BCQEWEmdjb25kcmFAZ29vZ2xlLmNvbYIJAPLf2gS0zYGUMAwGA1UdEwQF" + + "MAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAYiugFDmbDOQ2U/+mqNt7o8ftlEo9SJrns6O8uTtK6AvR" + + "orDrR1AXTXkuxwLSbmVfedMGOZy7Awh7iZa8hw5x9XmUudfNxvmrKVEwGQY2DZ9PXbrnta/dwbhK" + + "mWfoepESVbo7CKIhJp8gRW0h1Z55ETXD57aGJRvQS4pxkP8ANhM="; + + + public static final String TEST_KEY = "" + + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKMYYcBMFiWZqz7SwQQKSejwtnUo" + + "+CVv+Z97pWkJPcaFKv7tuvs0b9nAZmcSEuoTtggfh/GW0eZALdPP6MzVg36QxpCBFGOj8sEIl04p" + + "ozBRuRXKcAfo5iwp6+Os4+TyV8zlIVDMkwawD35N+imvnGDQHBnW8D/4TeYTtOvczZS/AgMBAAEC" + + "gYBxwFalNSwZK3WJipq+g6KLCiBn1JxGGDQlLKrweFaSuFyFky9fd3IvkIabirqQchD612sMb+GT" + + "0t1jptW6z4w2w6++IW0A3apDOCwoD+uvDBXrbFqI0VbyAWUNqHVdaFFIRk2IHGEE6463mGRdmILX" + + "IlCd/85RTHReg4rl/GFqWQJBANgLAIR4pWbl5Gm+DtY18wp6Q3pJAAMkmP/lISCBIidu1zcqYIKt" + + "PoDW4Knq9xnhxPbXrXKv4YzZWHBK8GkKhQ0CQQDBQnXufQcMew+PwiS0oJvS+eQ6YJwynuqG2ejg" + + "WE+T7489jKtscRATpUXpZUYmDLGg9bLt7L62hFvFSj2LO2X7AkBcdrD9AWnBFWlh/G77LVHczSEu" + + "KCoyLiqxcs5vy/TjLaQ8vw1ZQG580/qJnr+tOxyCjSJ18GK3VppsTRaBznfNAkB3nuCKNp9HTWCL" + + "dfrsRsFMrFpk++mSt6SoxXaMbn0LL2u1CD4PCEiQMGt+lK3/3TmRTKNs+23sYS7Ahjxj0udDAkEA" + + "p57Nj65WNaWeYiOfTwKXkLj8l29H5NbaGWxPT0XkWr4PvBOFZVH/wj0/qc3CMVGnv11+DyO+QUCN" + + "SqBB5aRe8g=="; + + private EditText actionEditText; + private EditText versionEditText; + private EditText contentPathEditText; + private EditText requiredHashEditText; + private TextView logView; + + private ExecutorService executor; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + Button triggerInstallButton = (Button) findViewById(R.id.trigger_install_button); + triggerInstallButton.setOnClickListener(this); + + actionEditText = (EditText) findViewById(R.id.action); + versionEditText = (EditText) findViewById(R.id.version); + contentPathEditText = (EditText) findViewById(R.id.content_path); + requiredHashEditText = (EditText) findViewById(R.id.required_hash); + logView = (TextView) findViewById(R.id.log); + executor = Executors.newFixedThreadPool(1); + } + + @Override + public void onClick(View v) { + final String action = actionEditText.getText().toString(); + final String contentPath = contentPathEditText.getText().toString(); + final String version = versionEditText.getText().toString(); + final String requiredHash = requiredHashEditText.getText().toString(); + + new AsyncTask<Void, String, Void>() { + @Override + protected Void doInBackground(Void... params) { + final File contentFile = new File(contentPath); + File tempDir = new File(getFilesDir(), "temp"); + if (!tempDir.exists() && !tempDir.mkdir()) { + publishProgress("Unable to create: " + tempDir); + return null; + } + + File copyOfContentFile; + try { + copyOfContentFile = File.createTempFile("content", ".tmp", tempDir); + copyFile(contentFile, copyOfContentFile); + } catch (IOException e) { + publishProgress("Error", exceptionToString(e)); + return null; + } + publishProgress("Created copy of " + contentFile + " at " + copyOfContentFile); + + String originalCert = null; + try { + originalCert = overrideCert(TEST_CERT); + sleep(1000); + publishProgress("Overridden update cert"); + + String signature = createSignature(copyOfContentFile, version, requiredHash); + sendIntent(copyOfContentFile, action, version, requiredHash, signature); + publishProgress("Sent update intent"); + } catch (Exception e) { + publishProgress("Error", exceptionToString(e)); + } finally { + if (originalCert != null) { + sleep(1000); + try { + overrideCert(originalCert); + publishProgress("Reverted update cert"); + } catch (Exception e) { + publishProgress("Unable to revert update cert", exceptionToString(e)); + } + } + } + publishProgress("Update intent sent successfully"); + return null; + } + + @Override + protected void onProgressUpdate(String... values) { + for (String message : values) { + addToLog(message, null); + } + } + }.executeOnExecutor(executor); + } + + private String overrideCert(String cert) throws Exception { + final String key = UPDATE_CERTIFICATE_KEY; + String originalCert = Settings.Secure.getString(getContentResolver(), key); + if (!Settings.Secure.putString(getContentResolver(), key, cert)) { + throw new Exception("Unable to override update certificate"); + } + return originalCert; + } + + private void sleep(long millisDelay) { + try { + Thread.sleep(millisDelay); + } catch (InterruptedException e) { + // Ignore + } + } + + private void sendIntent( + File contentFile, String action, String version, String required, String sig) { + Intent i = new Intent(); + i.setAction(action); + Uri contentUri = + FileProvider.getUriForFile( + getApplicationContext(), "libcore.tzdata.update_test_app.fileprovider", + contentFile); + i.setData(contentUri); + i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + i.putExtra(EXTRA_VERSION_NUMBER, version); + i.putExtra(EXTRA_REQUIRED_HASH, required); + i.putExtra(EXTRA_SIGNATURE, sig); + sendBroadcast(i); + } + + private void addToLog(String message, Exception e) { + logString(message); + if (e != null) { + String text = exceptionToString(e); + logString(text); + } + } + + private void logString(String value) { + logView.append(new Date() + " " + value + "\n"); + int scrollAmount = + logView.getLayout().getLineTop(logView.getLineCount()) - logView.getHeight(); + logView.scrollTo(0, scrollAmount); + } + + private static String createSignature(File contentFile, String version, String requiredHash) + throws Exception { + byte[] contentBytes = readBytes(contentFile); + Signature signer = Signature.getInstance("SHA512withRSA"); + signer.initSign(createKey()); + signer.update(contentBytes); + signer.update(version.trim().getBytes()); + signer.update(requiredHash.getBytes()); + return new String(Base64.encode(signer.sign(), Base64.DEFAULT)); + } + + private static byte[] readBytes(File contentFile) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (FileInputStream fis = new FileInputStream(contentFile)) { + int count; + byte[] buffer = new byte[8192]; + while ((count = fis.read(buffer)) != -1) { + baos.write(buffer, 0, count); + } + } + return baos.toByteArray(); + } + + private static PrivateKey createKey() throws Exception { + byte[] derKey = Base64.decode(TEST_KEY.getBytes(), Base64.DEFAULT); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(derKey); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(keySpec); + } + + private static String exceptionToString(Exception e) { + StringWriter writer = new StringWriter(); + e.printStackTrace(new PrintWriter(writer)); + return writer.getBuffer().toString(); + } + + private static void copyFile(File from, File to) throws IOException { + byte[] buffer = new byte[8192]; + int count; + try ( + FileInputStream in = new FileInputStream(from); + FileOutputStream out = new FileOutputStream(to) + ) { + while ((count = in.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + } + } +} |