not working version of otr file transfer

iNPUTmice created

Change summary

src/eu/siacs/conversations/entities/Conversation.java             |  10 
src/eu/siacs/conversations/parser/MessageParser.java              |  10 
src/eu/siacs/conversations/persistance/FileBackend.java           |   6 
src/eu/siacs/conversations/services/XmppConnectionService.java    |  47 
src/eu/siacs/conversations/ui/ContactsActivity.java               |   2 
src/eu/siacs/conversations/ui/ConversationActivity.java           |   5 
src/eu/siacs/conversations/ui/ConversationFragment.java           |   7 
src/eu/siacs/conversations/utils/CryptoHelper.java                |  18 
src/eu/siacs/conversations/utils/PRNGFixes.java                   | 326 +
src/eu/siacs/conversations/xmpp/XmppConnection.java               |  12 
src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java      |  24 
src/eu/siacs/conversations/xmpp/jingle/JingleFile.java            |  26 
src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java |  40 
src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java       |  59 
src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java       |   8 
15 files changed, 548 insertions(+), 52 deletions(-)

Detailed changes

src/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -62,6 +62,8 @@ public class Conversation extends AbstractEntity {
 
 	private transient String latestMarkableMessageId;
 
+	private byte[] symmetricKey;
+
 	public Conversation(String name, Account account, String contactJid,
 			int mode) {
 		this(java.util.UUID.randomUUID().toString(), name, null, account
@@ -353,4 +355,12 @@ public class Conversation extends AbstractEntity {
 			this.latestMarkableMessageId = id;
 		}
 	}
+
+	public void setSymmetricKey(byte[] key) {
+		this.symmetricKey = key;
+	}
+	
+	public byte[] getSymmetricKey() {
+		return this.symmetricKey;
+	}
 }

src/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -1,11 +1,13 @@
 package eu.siacs.conversations.parser;
 
+import android.util.Log;
 import net.java.otr4j.session.Session;
 import net.java.otr4j.session.SessionStatus;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
@@ -72,11 +74,15 @@ public class MessageParser extends AbstractParser {
 			} else if ((before != after) && (after == SessionStatus.FINISHED)) {
 				conversation.resetOtrSession();
 			}
-			// isEmpty is a work around for some weird clients which send emtpty
-			// strings over otr
 			if ((body == null) || (body.isEmpty())) {
 				return null;
 			}
+			if (body.startsWith(CryptoHelper.FILETRANSFER)) {
+				String key = body.substring(CryptoHelper.FILETRANSFER.length());
+				conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
+				Log.d("xmppService","new symmetric key: "+CryptoHelper.bytesToHex(conversation.getSymmetricKey()));
+				return null;
+			}
 			conversation.setLatestMarkableMessageId(getMarkableMessageId(packet));
 			Message finishedMessage = new Message(conversation, packet.getFrom(), body,
 					Message.ENCRYPTION_OTR, Message.STATUS_RECIEVED);

src/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -56,7 +56,11 @@ public class FileBackend {
 		if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
 			filename = message.getUuid() + ".webp";
 		} else {
-			filename = message.getUuid() + ".webp.pgp";
+			if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+				filename = message.getUuid() + ".webp";
+			} else {
+				filename = message.getUuid() + ".webp.pgp";
+			}
 		}
 		return new JingleFile(path + "/" + filename);
 	}

src/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -1,11 +1,11 @@
 package eu.siacs.conversations.services;
 
+import java.security.SecureRandom;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Locale;
-import java.util.Random;
 
 import org.openintents.openpgp.util.OpenPgpApi;
 import org.openintents.openpgp.util.OpenPgpServiceConnection;
@@ -28,8 +28,10 @@ import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.OnAccountListChangedListener;
 import eu.siacs.conversations.ui.OnConversationListChangedListener;
 import eu.siacs.conversations.ui.UiCallback;
+import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.ExceptionHelper;
 import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
+import eu.siacs.conversations.utils.PRNGFixes;
 import eu.siacs.conversations.utils.PhoneHelper;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xml.Element;
@@ -47,6 +49,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import android.annotation.SuppressLint;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -114,7 +117,7 @@ public class XmppConnectionService extends Service {
 		tlsException = listener;
 	}
 
-	private Random mRandom = new Random(System.currentTimeMillis());
+	private SecureRandom mRandom;
 
 	private long lastCarbonMessageReceived = -CARBON_GRACE_PERIOD;
 
@@ -367,7 +370,7 @@ public class XmppConnectionService extends Service {
 			message = new Message(conversation, "",
 					Message.ENCRYPTION_DECRYPTED);
 		} else {
-			message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+			message = new Message(conversation, "", conversation.getNextEncryption());
 		}
 		message.setPresence(conversation.getNextPresence());
 		message.setType(Message.TYPE_IMAGE);
@@ -509,9 +512,12 @@ public class XmppConnectionService extends Service {
 		return START_STICKY;
 	}
 
+	@SuppressLint("TrulyRandom")
 	@Override
 	public void onCreate() {
 		ExceptionHelper.init(getApplicationContext());
+		PRNGFixes.apply();
+		this.mRandom = new SecureRandom();
 		this.databaseBackend = DatabaseBackend
 				.getInstance(getApplicationContext());
 		this.fileBackend = new FileBackend(getApplicationContext());
@@ -604,7 +610,7 @@ public class XmppConnectionService extends Service {
 		SharedPreferences sharedPref = getPreferences();
 		account.setResource(sharedPref.getString("resource", "mobile")
 				.toLowerCase(Locale.getDefault()));
-		XmppConnection connection = new XmppConnection(account, this.pm);
+		XmppConnection connection = new XmppConnection(account, this);
 		connection.setOnMessagePacketReceivedListener(this.messageListener);
 		connection.setOnStatusChangedListener(this.statusListener);
 		connection.setOnPresencePacketReceivedListener(this.presenceListener);
@@ -1239,6 +1245,31 @@ public class XmppConnectionService extends Service {
 		}
 		updateUi(conversation, false);
 	}
+	
+	public boolean renewSymmetricKey(Conversation conversation) {
+		Account account = conversation.getAccount();
+		byte[] symmetricKey = new byte[32];
+		this.mRandom.nextBytes(symmetricKey);
+		Session otrSession = conversation.getOtrSession();
+		if (otrSession!=null) {
+			MessagePacket packet = new MessagePacket();
+			packet.setType(MessagePacket.TYPE_CHAT);
+			packet.setFrom(account.getFullJid());
+			packet.addChild("private", "urn:xmpp:carbons:2");
+			packet.addChild("no-copy", "urn:xmpp:hints");
+			packet.setTo(otrSession.getSessionID().getAccountID() + "/"
+					+ otrSession.getSessionID().getUserID());
+			try {
+				packet.setBody(otrSession.transformSending(CryptoHelper.FILETRANSFER+CryptoHelper.bytesToHex(symmetricKey)));
+				account.getXmppConnection().sendMessagePacket(packet);
+				conversation.setSymmetricKey(symmetricKey);
+				return true;
+			} catch (OtrException e) {
+				return false;
+			}
+		}
+		return false;
+	}
 
 	public void pushContactToServer(Contact contact) {
 		contact.resetOption(Contact.Options.DIRTY_DELETE);
@@ -1451,4 +1482,12 @@ public class XmppConnectionService extends Service {
 		received.setAttribute("id", id);
 		account.getXmppConnection().sendMessagePacket(receivedPacket);
 	}
+
+	public SecureRandom getRNG() {
+		return this.mRandom;
+	}
+
+	public PowerManager getPowerManager() {
+		return this.pm;
+	}
 }

src/eu/siacs/conversations/ui/ContactsActivity.java 🔗

@@ -237,7 +237,7 @@ public class ContactsActivity extends XmppActivity {
 	
 						@Override
 						public void onClick(DialogInterface dialog, int which) {
-							String mucName = CryptoHelper.randomMucName();
+							String mucName = CryptoHelper.randomMucName(xmppConnectionService.getRNG());
 							String serverName = account.getXmppConnection()
 									.getMucServer();
 							String jid = mucName + "@" + serverName;

src/eu/siacs/conversations/ui/ConversationActivity.java 🔗

@@ -418,7 +418,8 @@ public class ConversationActivity extends XmppActivity {
 		} else if (getSelectedConversation().getNextEncryption() == Message.ENCRYPTION_NONE) {
 			selectPresenceToAttachFile(attachmentChoice);
 		} else {
-			AlertDialog.Builder builder = new AlertDialog.Builder(this);
+			selectPresenceToAttachFile(attachmentChoice);
+			/*AlertDialog.Builder builder = new AlertDialog.Builder(this);
 			builder.setTitle(getString(R.string.otr_file_transfer));
 			builder.setMessage(getString(R.string.otr_file_transfer_msg));
 			builder.setNegativeButton(getString(R.string.cancel), null);
@@ -448,7 +449,7 @@ public class ConversationActivity extends XmppActivity {
 							}
 						});
 			}
-			builder.create().show();
+			builder.create().show();*/
 		}
 	}
 

src/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -316,7 +316,9 @@ public class ConversationFragment extends Fragment {
 			}
 
 			private void displayDecryptionFailed(ViewHolder viewHolder) {
-				viewHolder.download_button.setVisibility(View.GONE);
+				if (viewHolder.download_button != null) {
+					viewHolder.download_button.setVisibility(View.GONE);
+				}
 				viewHolder.image.setVisibility(View.GONE);
 				viewHolder.messageBody.setVisibility(View.VISIBLE);
 				viewHolder.messageBody
@@ -525,7 +527,8 @@ public class ConversationFragment extends Fragment {
 									}
 								});
 					} else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED)
-							|| (item.getEncryption() == Message.ENCRYPTION_NONE)) {
+							|| (item.getEncryption() == Message.ENCRYPTION_NONE)
+							|| (item.getEncryption() == Message.ENCRYPTION_OTR)) {
 						displayImageMessage(viewHolder, item);
 					} else if (item.getEncryption() == Message.ENCRYPTION_PGP) {
 						displayInfoMessage(viewHolder,

src/eu/siacs/conversations/utils/CryptoHelper.java 🔗

@@ -5,14 +5,14 @@ import java.nio.charset.Charset;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
-import java.util.Random;
 
 import eu.siacs.conversations.entities.Account;
 
 import android.util.Base64;
 
 public class CryptoHelper {
-	final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+	public static final String FILETRANSFER = "?FILETRANSFERv1:"; 
+	final protected static char[] hexArray = "0123456789abcdef".toCharArray();
 	final protected static char[] vowels = "aeiou".toCharArray();
 	final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz"
 			.toCharArray();
@@ -24,7 +24,11 @@ public class CryptoHelper {
 			hexChars[j * 2] = hexArray[v >>> 4];
 			hexChars[j * 2 + 1] = hexArray[v & 0x0F];
 		}
-		return new String(hexChars).toLowerCase();
+		return new String(hexChars);
+	}
+	
+	public static byte[] hexToBytes(String hexString) {
+		return new BigInteger(hexString, 16).toByteArray();
 	}
 
 	public static String saslPlain(String username, String password) {
@@ -40,9 +44,8 @@ public class CryptoHelper {
 	    return result;
 	} 
 	
-	public static String saslDigestMd5(Account account, String challenge) {
+	public static String saslDigestMd5(Account account, String challenge, SecureRandom random) {
 		try {
-			Random random = new SecureRandom();
 			String[] challengeParts = new String(Base64.decode(challenge,
 					Base64.DEFAULT)).split(",");
 			String nonce = "";
@@ -84,12 +87,11 @@ public class CryptoHelper {
 		}
 	}
 
-	public static String randomMucName() {
-		Random random = new SecureRandom();
+	public static String randomMucName(SecureRandom random) {
 		return randomWord(3, random) + "." + randomWord(7, random);
 	}
 
-	protected static String randomWord(int lenght, Random random) {
+	protected static String randomWord(int lenght, SecureRandom random) {
 		StringBuilder builder = new StringBuilder(lenght);
 		for (int i = 0; i < lenght; ++i) {
 			if (i % 2 == 0) {

src/eu/siacs/conversations/utils/PRNGFixes.java 🔗

@@ -0,0 +1,326 @@
+package eu.siacs.conversations.utils;
+
+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}.
+ */
+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");
+        }
+    }
+}

src/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -37,6 +37,7 @@ import android.os.PowerManager.WakeLock;
 import android.os.SystemClock;
 import android.util.Log;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.DNSHelper;
 import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
@@ -63,7 +64,7 @@ public class XmppConnection implements Runnable {
 
 	private WakeLock wakeLock;
 
-	private SecureRandom random = new SecureRandom();
+	private SecureRandom mRandom;
 
 	private Socket socket;
 	private XmlReader tagReader;
@@ -100,9 +101,10 @@ public class XmppConnection implements Runnable {
 	private OnTLSExceptionReceived tlsListener = null;
 	private OnBindListener bindListener = null;
 
-	public XmppConnection(Account account, PowerManager pm) {
+	public XmppConnection(Account account, XmppConnectionService service) {
+		this.mRandom = service.getRNG();
 		this.account = account;
-		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+		this.wakeLock = service.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
 				account.getJid());
 		tagWriter = new TagWriter();
 	}
@@ -248,7 +250,7 @@ public class XmppConnection implements Runnable {
 				response.setAttribute("xmlns",
 						"urn:ietf:params:xml:ns:xmpp-sasl");
 				response.setContent(CryptoHelper.saslDigestMd5(account,
-						challange));
+						challange,mRandom));
 				tagWriter.writeElement(response);
 			} else if (nextTag.isStart("enabled")) {
 				this.stanzasSent = 0;
@@ -772,7 +774,7 @@ public class XmppConnection implements Runnable {
 	}
 
 	private String nextRandomId() {
-		return new BigInteger(50, random).toString(32);
+		return new BigInteger(50, mRandom).toString(32);
 	}
 
 	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {

src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java 🔗

@@ -9,11 +9,11 @@ import java.util.Map.Entry;
 
 import android.graphics.BitmapFactory;
 import android.util.Log;
-
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
@@ -24,7 +24,7 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 public class JingleConnection {
 
 	private final String[] extensions = {"webp","jpeg","jpg","png"};
-	private final String[] cryptoExtensions = {"pgp","gpg"};
+	private final String[] cryptoExtensions = {"pgp","gpg","otr"};
 	
 	private JingleConnectionManager mJingleConnectionManager;
 	private XmppConnectionService mXmppConnectionService;
@@ -244,6 +244,7 @@ public class JingleConnection {
 			Element fileNameElement = fileOffer.findChild("name");
 			if (fileNameElement!=null) {
 				boolean supportedFile = false;
+				Log.d("xmppService","file offer: "+fileNameElement.getContent());
 				String[] filename = fileNameElement.getContent().toLowerCase().split("\\.");
 				if (Arrays.asList(this.extensions).contains(filename[filename.length - 1])) {
 					supportedFile = true;
@@ -251,7 +252,12 @@ public class JingleConnection {
 					if (filename.length == 3) {
 						if (Arrays.asList(this.extensions).contains(filename[filename.length -2])) {
 							supportedFile = true;
-							this.message.setEncryption(Message.ENCRYPTION_PGP);
+							if (filename[filename.length - 1].equals("otr")) {
+								Log.d("xmppService","receiving otr file");
+								this.message.setEncryption(Message.ENCRYPTION_OTR);
+							} else {
+								this.message.setEncryption(Message.ENCRYPTION_PGP);
+							}
 						}
 					}
 				}
@@ -269,6 +275,9 @@ public class JingleConnection {
 						this.mXmppConnectionService.updateUi(conversation, true);
 					}
 					this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message,false);
+					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+						this.file.setKey(conversation.getSymmetricKey());
+					}
 					this.file.setExpectedSize(size);
 				} else {
 					this.sendCancel();
@@ -287,7 +296,14 @@ public class JingleConnection {
 		if (message.getType() == Message.TYPE_IMAGE) {
 			content.setTransportId(this.transportId);
 			this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message,false);
-			content.setFileOffer(this.file);
+			if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+				Conversation conversation = this.message.getConversation();
+				this.mXmppConnectionService.renewSymmetricKey(conversation);
+				content.setFileOffer(this.file, true);
+				this.file.setKey(conversation.getSymmetricKey());
+			} else {
+				content.setFileOffer(this.file,false);
+			}
 			this.transportId = this.mJingleConnectionManager.nextRandomId();
 			content.setTransportId(this.transportId);
 			content.socks5transport().setChildren(getCandidatesAsElements());

src/eu/siacs/conversations/xmpp/jingle/JingleFile.java 🔗

@@ -1,6 +1,12 @@
 package eu.siacs.conversations.xmpp.jingle;
 
 import java.io.File;
+import java.security.Key;
+
+import javax.crypto.spec.SecretKeySpec;
+
+import eu.siacs.conversations.utils.CryptoHelper;
+import android.util.Log;
 
 public class JingleFile extends File {
 	
@@ -8,6 +14,7 @@ public class JingleFile extends File {
 	
 	private long expectedSize = 0;
 	private String sha1sum;
+	private Key aeskey;
 	
 	public JingleFile(String path) {
 		super(path);
@@ -32,4 +39,23 @@ public class JingleFile extends File {
 	public void setSha1Sum(String sum) {
 		this.sha1sum = sum;
 	}
+	
+	public void setKey(byte[] key) {
+		Log.d("xmppService","using aes key "+CryptoHelper.bytesToHex(key));
+		if (key.length>=32) {
+			byte[] secretKey = new byte[32];
+			System.arraycopy(key, 0, secretKey, 0, 32);
+			this.aeskey = new SecretKeySpec(key, "AES");
+		} else if (key.length>=16) {
+			byte[] secretKey = new byte[15];
+			System.arraycopy(key, 0, secretKey, 0, 16);
+			this.aeskey = new SecretKeySpec(key, "AES");
+		} else {
+			Log.d("xmppService","weird key");
+		}
+	}
+	
+	public Key getKey() {
+		return this.aeskey;
+	}
 }

src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java 🔗

@@ -12,6 +12,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 
+import android.util.Log;
 import eu.siacs.conversations.utils.CryptoHelper;
 
 public class JingleSocks5Transport extends JingleTransport {
@@ -90,19 +91,23 @@ public class JingleSocks5Transport extends JingleTransport {
 			
 			@Override
 			public void run() {
-				FileInputStream fileInputStream = null;
+				InputStream fileInputStream = null;
 				try {
 					MessageDigest digest = MessageDigest.getInstance("SHA-1");
 					digest.reset();
-					fileInputStream = new FileInputStream(file);
+					fileInputStream = getInputStream(file);
 					int count;
+					long txbytes = 0;
 					byte[] buffer = new byte[8192];
-					while ((count = fileInputStream.read(buffer)) > 0) {
+					while ((count = fileInputStream.read(buffer)) != -1) {
+						txbytes += count;
 						outputStream.write(buffer, 0, count);
 						digest.update(buffer, 0, count);
+						Log.d("xmppService","tx bytes: "+txbytes);
 					}
 					outputStream.flush();
 					file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+					//outputStream.close();
 					if (callback!=null) {
 						callback.onFileTransmitted(file);
 					}
@@ -110,8 +115,7 @@ public class JingleSocks5Transport extends JingleTransport {
 					// TODO Auto-generated catch block
 					e.printStackTrace();
 				} catch (IOException e) {
-					// TODO Auto-generated catch block
-					e.printStackTrace();
+					Log.d("xmppService","io exception: "+e.getMessage());
 				} catch (NoSuchAlgorithmException e) {
 					// TODO Auto-generated catch block
 					e.printStackTrace();
@@ -141,36 +145,30 @@ public class JingleSocks5Transport extends JingleTransport {
 					inputStream.skip(45);
 					file.getParentFile().mkdirs();
 					file.createNewFile();
-					FileOutputStream fileOutputStream = new FileOutputStream(file);
+					OutputStream fileOutputStream = getOutputStream(file);
 					long remainingSize = file.getExpectedSize();
 					byte[] buffer = new byte[8192];
 					int count = buffer.length;
-					while(remainingSize > 0) {
-						if (remainingSize<=count) {
-							count = (int) remainingSize;
-						}
-						count = inputStream.read(buffer, 0, count);
-						if (count==-1) {
-							// TODO throw exception
-						} else {
+					//while(remainingSize > 0) {
+					while((count = inputStream.read(buffer)) > 0) {
+						Log.d("xmppService","remaining size: "+remainingSize+" reading "+count+" bytes");
+						count = inputStream.read(buffer);
+						if (count!=-1) {
 							fileOutputStream.write(buffer, 0, count);
 							digest.update(buffer, 0, count);
-							remainingSize-=count;
 						}
+						remainingSize-=count;
 					}
 					fileOutputStream.flush();
 					fileOutputStream.close();
 					file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
 					callback.onFileTransmitted(file);
 				} catch (FileNotFoundException e) {
-					// TODO Auto-generated catch block
-					e.printStackTrace();
+					Log.d("xmppService","file not found exception");
 				} catch (IOException e) {
-					// TODO Auto-generated catch block
-					e.printStackTrace();
+					Log.d("xmppService","io exception: "+e.getMessage());
 				} catch (NoSuchAlgorithmException e) {
-					// TODO Auto-generated catch block
-					e.printStackTrace();
+					Log.d("xmppService","no such algo"+e.getMessage());
 				}
 			}
 		}).start();

src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java 🔗

@@ -1,7 +1,66 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.CipherInputStream;
+import javax.crypto.NoSuchPaddingException;
+
+import android.util.Log;
+
 public abstract class JingleTransport {
 	public abstract void connect(final OnTransportConnected callback);
 	public abstract void receive(final JingleFile file, final OnFileTransmitted callback);
 	public abstract void send(final JingleFile file, final OnFileTransmitted callback);
+	
+	protected InputStream getInputStream(JingleFile file) throws FileNotFoundException {
+		if (file.getKey() == null) {
+			return new FileInputStream(file);
+		} else {
+			try {
+				Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+				cipher.init(Cipher.ENCRYPT_MODE, file.getKey());
+				Log.d("xmppService","opening encrypted input stream");
+				return new CipherInputStream(new FileInputStream(file), cipher);
+			} catch (NoSuchAlgorithmException e) {
+				Log.d("xmppService","no such algo: "+e.getMessage());
+				return null;
+			} catch (NoSuchPaddingException e) {
+				Log.d("xmppService","no such padding: "+e.getMessage());
+				return null;
+			} catch (InvalidKeyException e) {
+				Log.d("xmppService","invalid key: "+e.getMessage());
+				return null;
+			}
+		}
+	}
+	
+	protected OutputStream getOutputStream(JingleFile file) throws FileNotFoundException {
+		if (file.getKey() == null) {
+			return new FileOutputStream(file);
+		} else {
+			try {
+				Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+				cipher.init(Cipher.DECRYPT_MODE, file.getKey());
+				Log.d("xmppService","opening encrypted output stream");
+				return new CipherOutputStream(new FileOutputStream(file), cipher);
+			} catch (NoSuchAlgorithmException e) {
+				Log.d("xmppService","no such algo: "+e.getMessage());
+				return null;
+			} catch (NoSuchPaddingException e) {
+				Log.d("xmppService","no such padding: "+e.getMessage());
+				return null;
+			} catch (InvalidKeyException e) {
+				Log.d("xmppService","invalid key: "+e.getMessage());
+				return null;
+			}
+		}
+	}
 }

src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java 🔗

@@ -25,12 +25,16 @@ public class Content extends Element {
 		this.transportId = sid;
 	}
 	
-	public void setFileOffer(JingleFile actualFile) {
+	public void setFileOffer(JingleFile actualFile, boolean otr) {
 		Element description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3");
 		Element offer = description.addChild("offer");
 		Element file = offer.addChild("file");
 		file.addChild("size").setContent(""+actualFile.getSize());
-		file.addChild("name").setContent(actualFile.getName());
+		if (otr) {
+			file.addChild("name").setContent(actualFile.getName()+".otr");
+		} else {
+			file.addChild("name").setContent(actualFile.getName());
+		}
 	}
 	
 	public Element getFileOffer() {