diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java new file mode 100644 index 0000000..00020b1 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java @@ -0,0 +1,84 @@ +package ch.dissem.apps.abit.adapter; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import ch.dissem.apps.abit.util.PRNGFixes; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.PlaintextHolder; +import ch.dissem.bitmessage.entity.payload.Broadcast; +import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.ProofOfWorkEngine; +import ch.dissem.bitmessage.security.sc.SpongySecurity; +import ch.dissem.bitmessage.utils.UnixTime; + +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; +import static ch.dissem.bitmessage.entity.Plaintext.Status.SENT; +import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; +import static ch.dissem.bitmessage.utils.UnixTime.DAY; + +/** + * @author Christian Basler + */ +public class AndroidSecurity extends SpongySecurity { + private final SharedPreferences preferences; + + public AndroidSecurity(Context ctx) { + PRNGFixes.apply(); + preferences = PreferenceManager.getDefaultSharedPreferences(ctx); + } + + @Override + public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes, ProofOfWorkEngine.Callback callback) { + if (preferences.getBoolean(PREFERENCE_SERVER_POW, false)) { + object.setNonce(new byte[8]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + object.write(out); + sendAsBroadcast(getContext().getAddressRepo().getIdentities().get(0), out.toByteArray()); + if (object.getPayload() instanceof PlaintextHolder) { + Plaintext plaintext = ((PlaintextHolder) object.getPayload()).getPlaintext(); + plaintext.setInventoryVector(object.getInventoryVector()); + plaintext.setStatus(SENT); + plaintext.removeLabel(Label.Type.OUTBOX); + plaintext.addLabels(getContext().getMessageRepository().getLabels(Label.Type.SENT)); + getContext().getMessageRepository().save(plaintext); + + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + super.doProofOfWork(object, nonceTrialsPerByte, extraBytes, callback); + } + } + + private void sendAsBroadcast(BitmessageAddress identity, byte[] data) throws IOException { + Plaintext msg = new Plaintext.Builder(BROADCAST) + .from(identity) + .message(data) + .build(); + Broadcast payload = Factory.getBroadcast(identity, msg); + long expires = UnixTime.now(+2 * DAY); + final ObjectMessage object = new ObjectMessage.Builder() + .stream(identity.getStream()) + .expiresTime(expires) + .payload(payload) + .build(); + object.sign(identity.getPrivateKey()); + payload.encrypt(); + object.setNonce(new byte[8]); + + getContext().getInventory().storeObject(object); + getContext().getNetworkHandler().offer(object.getInventoryVector()); + // TODO: offer to the trusted node only? + // at least make sure it is offered to the trusted node! + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index a178519..943c3e5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -2,6 +2,7 @@ package ch.dissem.apps.abit.service; import android.content.Context; +import ch.dissem.apps.abit.adapter.AndroidSecurity; import ch.dissem.apps.abit.listener.MessageListener; import ch.dissem.apps.abit.repository.AndroidAddressRepository; import ch.dissem.apps.abit.repository.AndroidInventory; @@ -30,7 +31,7 @@ public class Singleton { SqlHelper sqlHelper = new SqlHelper(ctx); bitmessageContext = new BitmessageContext.Builder() .proofOfWorkEngine(new ServicePowEngine(ctx)) - .security(new SpongySecurity()) + .security(new AndroidSecurity(ctx)) .nodeRegistry(new MemoryNodeRegistry()) .inventory(new AndroidInventory(sqlHelper)) .addressRepo(new AndroidAddressRepository(sqlHelper)) diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index 7654833..c1c55c5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -20,6 +20,9 @@ import ch.dissem.apps.abit.notification.ErrorNotification; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; + /** * Sync Adapter to synchronize with the Bitmessage network - fetches * new objects and then disconnects. @@ -48,7 +51,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - String trustedNode = preferences.getString("trusted_node", null); + String trustedNode = preferences.getString(PREFERENCE_TRUSTED_NODE, null); if (trustedNode == null) return; trustedNode = trustedNode.trim(); if (trustedNode.isEmpty()) return; @@ -69,7 +72,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { } else { port = 8444; } - long timeoutInSeconds = Long.parseLong(preferences.getString("sync_timeout", "120")); + long timeoutInSeconds = Long.parseLong(preferences.getString(PREFERENCE_SYNC_TIMEOUT, "120")); try { LOG.info("Synchronization started"); bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, true); diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Constants.java b/app/src/main/java/ch/dissem/apps/abit/util/Constants.java index 436dd6c..20fcbee 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Constants.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Constants.java @@ -3,9 +3,14 @@ package ch.dissem.apps.abit.util; import java.util.regex.Pattern; /** - * Created by chrigu on 16.11.15. + * @author Christian Basler */ public class Constants { + public static final String PREFERENCE_WIFI_ONLY = "wifi_only"; + public static final String PREFERENCE_TRUSTED_NODE = "trusted_node"; + public static final String PREFERENCE_SYNC_TIMEOUT = "sync_timeout"; + public static final String PREFERENCE_SERVER_POW = "server_pow"; + public static final String BITMESSAGE_URL_SCHEMA = "bitmessage:"; public static final Pattern BITMESSAGE_ADDRESS_PATTERN = Pattern.compile("\\bBM-[a-zA-Z0-9]+\\b"); } diff --git a/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java b/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java new file mode 100644 index 0000000..a17d0a2 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java @@ -0,0 +1,341 @@ +package ch.dissem.apps.abit.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}. + * + * @see + * http://android-developers.blogspot.ch/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/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index da9a684..19fda0d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -49,4 +49,6 @@