aboutsummaryrefslogtreecommitdiffstats
path: root/src/eu
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/eu/siacs/conversations/entities/Conversation.java10
-rw-r--r--src/eu/siacs/conversations/parser/MessageParser.java10
-rw-r--r--src/eu/siacs/conversations/persistance/FileBackend.java6
-rw-r--r--src/eu/siacs/conversations/services/XmppConnectionService.java47
-rw-r--r--src/eu/siacs/conversations/ui/ContactsActivity.java2
-rw-r--r--src/eu/siacs/conversations/ui/ConversationActivity.java5
-rw-r--r--src/eu/siacs/conversations/ui/ConversationFragment.java7
-rw-r--r--src/eu/siacs/conversations/utils/CryptoHelper.java18
-rw-r--r--src/eu/siacs/conversations/utils/PRNGFixes.java326
-rw-r--r--src/eu/siacs/conversations/xmpp/XmppConnection.java12
-rw-r--r--src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java24
-rw-r--r--src/eu/siacs/conversations/xmpp/jingle/JingleFile.java26
-rw-r--r--src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java40
-rw-r--r--src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java59
-rw-r--r--src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java8
15 files changed, 548 insertions, 52 deletions
diff --git a/src/eu/siacs/conversations/entities/Conversation.java b/src/eu/siacs/conversations/entities/Conversation.java
index 0d344c7f..640d89e9 100644
--- a/src/eu/siacs/conversations/entities/Conversation.java
+++ b/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;
+ }
}
diff --git a/src/eu/siacs/conversations/parser/MessageParser.java b/src/eu/siacs/conversations/parser/MessageParser.java
index fdd7db64..a435d055 100644
--- a/src/eu/siacs/conversations/parser/MessageParser.java
+++ b/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);
diff --git a/src/eu/siacs/conversations/persistance/FileBackend.java b/src/eu/siacs/conversations/persistance/FileBackend.java
index 82a7a3f9..ca6360b9 100644
--- a/src/eu/siacs/conversations/persistance/FileBackend.java
+++ b/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);
}
diff --git a/src/eu/siacs/conversations/services/XmppConnectionService.java b/src/eu/siacs/conversations/services/XmppConnectionService.java
index aeba113b..505d09e5 100644
--- a/src/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/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;
+ }
}
diff --git a/src/eu/siacs/conversations/ui/ContactsActivity.java b/src/eu/siacs/conversations/ui/ContactsActivity.java
index 0047513d..4e9c8af6 100644
--- a/src/eu/siacs/conversations/ui/ContactsActivity.java
+++ b/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;
diff --git a/src/eu/siacs/conversations/ui/ConversationActivity.java b/src/eu/siacs/conversations/ui/ConversationActivity.java
index 59c8fc4f..4e264df7 100644
--- a/src/eu/siacs/conversations/ui/ConversationActivity.java
+++ b/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();*/
}
}
diff --git a/src/eu/siacs/conversations/ui/ConversationFragment.java b/src/eu/siacs/conversations/ui/ConversationFragment.java
index f80f93dd..2eac7ad0 100644
--- a/src/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/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,
diff --git a/src/eu/siacs/conversations/utils/CryptoHelper.java b/src/eu/siacs/conversations/utils/CryptoHelper.java
index ac189fc1..408c32fe 100644
--- a/src/eu/siacs/conversations/utils/CryptoHelper.java
+++ b/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) {
diff --git a/src/eu/siacs/conversations/utils/PRNGFixes.java b/src/eu/siacs/conversations/utils/PRNGFixes.java
new file mode 100644
index 00000000..cf28ff30
--- /dev/null
+++ b/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");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java
index f2c0962a..2447b49b 100644
--- a/src/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/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) {
diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
index 8f88688a..85110c38 100644
--- a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
+++ b/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());
diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java b/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java
index 21cbd716..fd366754 100644
--- a/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java
+++ b/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;
+ }
}
diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
index 0f2d4cae..838c7d5f 100644
--- a/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
+++ b/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();
diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
index 290bb5d3..b96fefed 100644
--- a/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
+++ b/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;
+ }
+ }
+ }
}
diff --git a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
index abede91a..494ff0d6 100644
--- a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
+++ b/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() {