not working version of otr file transfer

This commit is contained in:
iNPUTmice 2014-06-20 17:30:19 +02:00
parent beafb06b6a
commit 1cf055d2fd
15 changed files with 548 additions and 52 deletions

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
package eu.siacs.conversations.services; package eu.siacs.conversations.services;
import java.security.SecureRandom;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Random;
import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection; 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.OnAccountListChangedListener;
import eu.siacs.conversations.ui.OnConversationListChangedListener; import eu.siacs.conversations.ui.OnConversationListChangedListener;
import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
import eu.siacs.conversations.utils.PRNGFixes;
import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element; 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.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
import android.annotation.SuppressLint;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
@ -114,7 +117,7 @@ public class XmppConnectionService extends Service {
tlsException = listener; tlsException = listener;
} }
private Random mRandom = new Random(System.currentTimeMillis()); private SecureRandom mRandom;
private long lastCarbonMessageReceived = -CARBON_GRACE_PERIOD; private long lastCarbonMessageReceived = -CARBON_GRACE_PERIOD;
@ -367,7 +370,7 @@ public class XmppConnectionService extends Service {
message = new Message(conversation, "", message = new Message(conversation, "",
Message.ENCRYPTION_DECRYPTED); Message.ENCRYPTION_DECRYPTED);
} else { } else {
message = new Message(conversation, "", Message.ENCRYPTION_NONE); message = new Message(conversation, "", conversation.getNextEncryption());
} }
message.setPresence(conversation.getNextPresence()); message.setPresence(conversation.getNextPresence());
message.setType(Message.TYPE_IMAGE); message.setType(Message.TYPE_IMAGE);
@ -509,9 +512,12 @@ public class XmppConnectionService extends Service {
return START_STICKY; return START_STICKY;
} }
@SuppressLint("TrulyRandom")
@Override @Override
public void onCreate() { public void onCreate() {
ExceptionHelper.init(getApplicationContext()); ExceptionHelper.init(getApplicationContext());
PRNGFixes.apply();
this.mRandom = new SecureRandom();
this.databaseBackend = DatabaseBackend this.databaseBackend = DatabaseBackend
.getInstance(getApplicationContext()); .getInstance(getApplicationContext());
this.fileBackend = new FileBackend(getApplicationContext()); this.fileBackend = new FileBackend(getApplicationContext());
@ -604,7 +610,7 @@ public class XmppConnectionService extends Service {
SharedPreferences sharedPref = getPreferences(); SharedPreferences sharedPref = getPreferences();
account.setResource(sharedPref.getString("resource", "mobile") account.setResource(sharedPref.getString("resource", "mobile")
.toLowerCase(Locale.getDefault())); .toLowerCase(Locale.getDefault()));
XmppConnection connection = new XmppConnection(account, this.pm); XmppConnection connection = new XmppConnection(account, this);
connection.setOnMessagePacketReceivedListener(this.messageListener); connection.setOnMessagePacketReceivedListener(this.messageListener);
connection.setOnStatusChangedListener(this.statusListener); connection.setOnStatusChangedListener(this.statusListener);
connection.setOnPresencePacketReceivedListener(this.presenceListener); connection.setOnPresencePacketReceivedListener(this.presenceListener);
@ -1240,6 +1246,31 @@ public class XmppConnectionService extends Service {
updateUi(conversation, false); 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) { public void pushContactToServer(Contact contact) {
contact.resetOption(Contact.Options.DIRTY_DELETE); contact.resetOption(Contact.Options.DIRTY_DELETE);
Account account = contact.getAccount(); Account account = contact.getAccount();
@ -1451,4 +1482,12 @@ public class XmppConnectionService extends Service {
received.setAttribute("id", id); received.setAttribute("id", id);
account.getXmppConnection().sendMessagePacket(receivedPacket); account.getXmppConnection().sendMessagePacket(receivedPacket);
} }
public SecureRandom getRNG() {
return this.mRandom;
}
public PowerManager getPowerManager() {
return this.pm;
}
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -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");
}
}
}

View file

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

View file

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

View file

@ -1,6 +1,12 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import java.io.File; 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 { public class JingleFile extends File {
@ -8,6 +14,7 @@ public class JingleFile extends File {
private long expectedSize = 0; private long expectedSize = 0;
private String sha1sum; private String sha1sum;
private Key aeskey;
public JingleFile(String path) { public JingleFile(String path) {
super(path); super(path);
@ -32,4 +39,23 @@ public class JingleFile extends File {
public void setSha1Sum(String sum) { public void setSha1Sum(String sum) {
this.sha1sum = 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;
}
} }

View file

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

View file

@ -1,7 +1,66 @@
package eu.siacs.conversations.xmpp.jingle; 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 class JingleTransport {
public abstract void connect(final OnTransportConnected callback); public abstract void connect(final OnTransportConnected callback);
public abstract void receive(final JingleFile file, final OnFileTransmitted callback); public abstract void receive(final JingleFile file, final OnFileTransmitted callback);
public abstract void send(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;
}
}
}
} }

View file

@ -25,13 +25,17 @@ public class Content extends Element {
this.transportId = sid; 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 description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3");
Element offer = description.addChild("offer"); Element offer = description.addChild("offer");
Element file = offer.addChild("file"); Element file = offer.addChild("file");
file.addChild("size").setContent(""+actualFile.getSize()); file.addChild("size").setContent(""+actualFile.getSize());
if (otr) {
file.addChild("name").setContent(actualFile.getName()+".otr");
} else {
file.addChild("name").setContent(actualFile.getName()); file.addChild("name").setContent(actualFile.getName());
} }
}
public Element getFileOffer() { public Element getFileOffer() {
Element description = this.findChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); Element description = this.findChild("description", "urn:xmpp:jingle:apps:file-transfer:3");