From d565bdb373cf2fe6a18b76c56788db77bb8f24f3 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 18 Nov 2014 13:06:36 +0200 Subject: [PATCH] Fixed various lint problems, improved navigation (ActionBar up arrows). --- app/build.gradle | 1 + app/lint.xml | 10 + app/src/main/AndroidManifest.xml | 21 +- .../com/nutomic/ensichat/util/PRNGFixes.java | 338 ++++++++++++++++++ app/src/main/res/layout/fragment_chat.xml | 3 +- .../activities/AddContactsActivity.scala | 10 + .../ensichat/activities/MainActivity.scala | 11 + .../activities/SettingsActivity.scala | 10 + .../nutomic/ensichat/messages/Crypto.scala | 3 + 9 files changed, 399 insertions(+), 8 deletions(-) create mode 100644 app/lint.xml create mode 100644 app/src/main/java/com/nutomic/ensichat/util/PRNGFixes.java diff --git a/app/build.gradle b/app/build.gradle index 4b5bae7..a103f96 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,7 @@ buildscript { } dependencies { + compile "com.android.support:support-v4:21.0.0" compile "org.scala-lang:scala-library:2.11.4" compile "org.msgpack:msgpack-scala_2.11:0.6.11" } diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000..df1f06b --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bfff160..c7783cd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.nutomic.ensichat" + tools:ignore="InvalidPackage" > @@ -15,7 +17,8 @@ + android:label="@string/app_name" + android:launchMode="singleTop" > @@ -25,13 +28,19 @@ + android:label="@string/add_contacts" > + + + android:label="@string/settings" > + + diff --git a/app/src/main/java/com/nutomic/ensichat/util/PRNGFixes.java b/app/src/main/java/com/nutomic/ensichat/util/PRNGFixes.java new file mode 100644 index 0000000..c38c827 --- /dev/null +++ b/app/src/main/java/com/nutomic/ensichat/util/PRNGFixes.java @@ -0,0 +1,338 @@ +package com.nutomic.ensichat.util; + +/* + * This software is provided 'as-is', without any express or implied + * warranty. In no event will Google be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, as long as the origin is not misrepresented. + */ + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + * + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + * + * Copied from: http://android-developers.blogspot.fi/2013/08/some-securerandom-thoughts.html + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = + getBuildFingerprintAndDeviceSerial(); + + /** Hidden constructor to prevent instantiation. */ + private PRNGFixes() {} + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = + Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals( + secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng1.getProvider().getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", + 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat.xml b/app/src/main/res/layout/fragment_chat.xml index 539130b..59f4868 100644 --- a/app/src/main/res/layout/fragment_chat.xml +++ b/app/src/main/res/layout/fragment_chat.xml @@ -50,8 +50,7 @@ android:id="@+id/send" android:layout_width="45dp" android:layout_height="45dp" - android:background="@drawable/ic_action_send_now" - android:onClick="sendMessage"/> + android:background="@drawable/ic_action_send_now"/> diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala index 0959675..68ffe23 100644 --- a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala +++ b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala @@ -6,6 +6,7 @@ import android.app.AlertDialog import android.content.DialogInterface.OnClickListener import android.content.{Context, DialogInterface} import android.os.Bundle +import android.support.v4.app.NavUtils import android.util.Log import android.view._ import android.widget.AdapterView.OnItemClickListener @@ -54,6 +55,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection */ override def onCreate(savedInstanceState: Bundle): Unit = { super.onCreate(savedInstanceState) + getActionBar.setDisplayHomeAsUpEnabled(true) setContentView(R.layout.activity_add_contacts) val list = findViewById(android.R.id.list).asInstanceOf[ListView] @@ -191,4 +193,12 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection } } + override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match { + case android.R.id.home => + NavUtils.navigateUpFromSameTask(this) + true; + case _ => + super.onOptionsItemSelected(item); + } + } diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala index 894d024..2d8cdaa 100644 --- a/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala +++ b/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala @@ -4,6 +4,7 @@ import android.app.Activity import android.bluetooth.BluetoothAdapter import android.content._ import android.os.Bundle +import android.view.MenuItem import android.widget.Toast import com.nutomic.ensichat.R import com.nutomic.ensichat.bluetooth.Device @@ -80,6 +81,7 @@ class MainActivity extends EnsiChatActivity { .detach(ContactsFragment) .add(android.R.id.content, new ChatFragment(device)) .commit() + getActionBar.setDisplayHomeAsUpEnabled(true) } /** @@ -93,8 +95,17 @@ class MainActivity extends EnsiChatActivity { .attach(ContactsFragment) .commit() currentChat = None + getActionBar.setDisplayHomeAsUpEnabled(false) } else super.onBackPressed() } + override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match { + case android.R.id.home => + onBackPressed() + true; + case _ => + super.onOptionsItemSelected(item); + } + } diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/SettingsActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/SettingsActivity.scala index 31cb3b3..ca5cac8 100644 --- a/app/src/main/scala/com/nutomic/ensichat/activities/SettingsActivity.scala +++ b/app/src/main/scala/com/nutomic/ensichat/activities/SettingsActivity.scala @@ -2,6 +2,8 @@ package com.nutomic.ensichat.activities import android.app.Fragment import android.os.Bundle +import android.support.v4.app.NavUtils +import android.view.MenuItem import com.nutomic.ensichat.fragments.SettingsFragment /** @@ -33,4 +35,12 @@ class SettingsActivity extends EnsiChatActivity { getFragmentManager.putFragment(outState, "settings_fragment", fragment) } + override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match { + case android.R.id.home => + NavUtils.navigateUpFromSameTask(this) + true; + case _ => + super.onOptionsItemSelected(item); + } + } diff --git a/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala b/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala index 7963c14..03cff06 100644 --- a/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala +++ b/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala @@ -9,6 +9,7 @@ import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey} import android.util.Log import com.nutomic.ensichat.bluetooth.Device import com.nutomic.ensichat.messages.Crypto._ +import com.nutomic.ensichat.util.PRNGFixes object Crypto { @@ -54,6 +55,8 @@ class Crypto(filesDir: File) { private val PublicKeyAlias = "local-public" + PRNGFixes.apply() + /** * Generates a new key pair using [[KeyAlgorithm]] with [[KeySize]] bits and stores the keys. */