diff options
Diffstat (limited to 'src/eu')
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() { |