From 303d1acd4554d87d920a265fec7cc167a8bb094e Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sun, 31 Mar 2013 19:16:06 -0700 Subject: [PATCH] Initial client support for GCM message send/receive --- AndroidManifest.xml | 2 +- res/layout/registration_progress_activity.xml | 42 ++++ .../securesms/AutoInitiateActivity.java | 4 +- .../RegistrationProgressActivity.java | 56 +++++- .../securesms/RoutingActivity.java | 28 +-- .../securesms/VerifyIdentityActivity.java | 2 +- .../securesms/VerifyKeysActivity.java | 2 +- .../crypto/AuthenticityCalculator.java | 2 +- .../crypto/KeyExchangeInitiator.java | 2 +- .../securesms/crypto/KeyExchangeMessage.java | 2 +- .../crypto/KeyExchangeProcessor.java | 6 +- .../securesms/crypto/KeyUtil.java | 6 +- .../securesms/crypto/SessionCipher.java | 10 +- .../database/EncryptingSmsDatabase.java | 6 +- .../securesms/database/SmsDatabase.java | 18 +- .../{ => keys}/InvalidKeyIdException.java | 2 +- .../database/{ => keys}/LocalKeyRecord.java | 5 +- .../securesms/database/{ => keys}/Record.java | 2 +- .../database/{ => keys}/RemoteKeyRecord.java | 5 +- .../database/{ => keys}/SessionKey.java | 2 +- .../database/{ => keys}/SessionRecord.java | 5 +- .../securesms/directory/BloomFilter.java | 86 +++++++++ .../directory/DirectoryDescriptor.java | 26 +++ .../securesms/directory/NumberFilter.java | 181 ++++++++++++++++++ .../securesms/gcm/GcmIntentService.java | 53 ++++- .../securesms/gcm/GcmMessageResponse.java | 18 ++ .../thoughtcrime/securesms/gcm/GcmSender.java | 20 -- ...RegistrationSocket.java => GcmSocket.java} | 172 ++++++++++++----- .../securesms/gcm/IncomingGcmMessage.java | 42 ++++ .../securesms/gcm/OptimizingTransport.java | 79 ++++++++ .../securesms/gcm/OutgoingGcmMessage.java | 37 ++++ .../service/RegistrationService.java | 15 +- .../securesms/service/SendReceiveService.java | 10 +- .../securesms/service/SmsListener.java | 22 ++- .../securesms/service/SmsReceiver.java | 50 ++--- .../securesms/service/SmsSender.java | 11 +- .../securesms/sms/TextMessage.java | 104 ++++++++++ .../securesms/util/Conversions.java | 8 + 38 files changed, 964 insertions(+), 179 deletions(-) rename src/org/thoughtcrime/securesms/database/{ => keys}/InvalidKeyIdException.java (96%) rename src/org/thoughtcrime/securesms/database/{ => keys}/LocalKeyRecord.java (95%) rename src/org/thoughtcrime/securesms/database/{ => keys}/Record.java (98%) rename src/org/thoughtcrime/securesms/database/{ => keys}/RemoteKeyRecord.java (95%) rename src/org/thoughtcrime/securesms/database/{ => keys}/SessionKey.java (98%) rename src/org/thoughtcrime/securesms/database/{ => keys}/SessionRecord.java (96%) create mode 100644 src/org/thoughtcrime/securesms/directory/BloomFilter.java create mode 100644 src/org/thoughtcrime/securesms/directory/DirectoryDescriptor.java create mode 100644 src/org/thoughtcrime/securesms/directory/NumberFilter.java create mode 100644 src/org/thoughtcrime/securesms/gcm/GcmMessageResponse.java delete mode 100644 src/org/thoughtcrime/securesms/gcm/GcmSender.java rename src/org/thoughtcrime/securesms/gcm/{RegistrationSocket.java => GcmSocket.java} (55%) create mode 100644 src/org/thoughtcrime/securesms/gcm/IncomingGcmMessage.java create mode 100644 src/org/thoughtcrime/securesms/gcm/OptimizingTransport.java create mode 100644 src/org/thoughtcrime/securesms/gcm/OutgoingGcmMessage.java create mode 100644 src/org/thoughtcrime/securesms/sms/TextMessage.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d415492c83..33be2302ff 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -34,7 +34,7 @@ - diff --git a/res/layout/registration_progress_activity.xml b/res/layout/registration_progress_activity.xml index 3cf4797711..4032aa0a5b 100644 --- a/res/layout/registration_progress_activity.xml +++ b/res/layout/registration_progress_activity.xml @@ -456,6 +456,48 @@ android:textSize="16.0sp" /> + + + + + + + + + + + recipientList = new ArrayList(1); - recipientList.add(new Recipient(null, message.getDisplayOriginatingAddress(), null, null)); + recipientList.add(new Recipient(null, message.getSender(), null, null)); Recipients recipients = new Recipients(recipientList); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); ContentValues values = new ContentValues(6); - values.put(ADDRESS, message.getDisplayOriginatingAddress()); + values.put(ADDRESS, message.getSender()); values.put(DATE_RECEIVED, Long.valueOf(System.currentTimeMillis())); values.put(DATE_SENT, timeSent); - values.put(PROTOCOL, message.getProtocolIdentifier()); + values.put(PROTOCOL, message.getProtocol()); values.put(READ, Integer.valueOf(0)); if (message.getPseudoSubject().length() > 0) @@ -212,13 +212,13 @@ public class SmsDatabase extends Database { notifyConversationListListeners(); } - public long insertSecureMessageReceived(SmsMessage message, String body) { + public long insertSecureMessageReceived(TextMessage message, String body) { return insertMessageReceived(message, body, Types.DECRYPT_IN_PROGRESS_TYPE, - message.getTimestampMillis()); + message.getSentTimestampMillis()); } - public long insertMessageReceived(SmsMessage message, String body) { - return insertMessageReceived(message, body, Types.INBOX_TYPE, message.getTimestampMillis()); + public long insertMessageReceived(TextMessage message, String body) { + return insertMessageReceived(message, body, Types.INBOX_TYPE, message.getSentTimestampMillis()); } public long insertMessageSent(String address, long threadId, String body, long date, long type) { diff --git a/src/org/thoughtcrime/securesms/database/InvalidKeyIdException.java b/src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java similarity index 96% rename from src/org/thoughtcrime/securesms/database/InvalidKeyIdException.java rename to src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java index aa34024b90..c46a8897b9 100644 --- a/src/org/thoughtcrime/securesms/database/InvalidKeyIdException.java +++ b/src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.database; +package org.thoughtcrime.securesms.database.keys; public class InvalidKeyIdException extends Exception { diff --git a/src/org/thoughtcrime/securesms/database/LocalKeyRecord.java b/src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java similarity index 95% rename from src/org/thoughtcrime/securesms/database/LocalKeyRecord.java rename to src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java index 67b84b8430..737b704c22 100644 --- a/src/org/thoughtcrime/securesms/database/LocalKeyRecord.java +++ b/src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.database; +package org.thoughtcrime.securesms.database.keys; import android.content.Context; import android.util.Log; @@ -24,6 +24,9 @@ import org.thoughtcrime.securesms.crypto.KeyPair; import org.thoughtcrime.securesms.crypto.KeyUtil; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.CanonicalAddressDatabase; +import org.thoughtcrime.securesms.database.keys.InvalidKeyIdException; +import org.thoughtcrime.securesms.database.keys.Record; import org.thoughtcrime.securesms.recipients.Recipient; import java.io.FileInputStream; diff --git a/src/org/thoughtcrime/securesms/database/Record.java b/src/org/thoughtcrime/securesms/database/keys/Record.java similarity index 98% rename from src/org/thoughtcrime/securesms/database/Record.java rename to src/org/thoughtcrime/securesms/database/keys/Record.java index 5f45ecae24..7c5b0d0603 100644 --- a/src/org/thoughtcrime/securesms/database/Record.java +++ b/src/org/thoughtcrime/securesms/database/keys/Record.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.database; +package org.thoughtcrime.securesms.database.keys; import android.content.Context; diff --git a/src/org/thoughtcrime/securesms/database/RemoteKeyRecord.java b/src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java similarity index 95% rename from src/org/thoughtcrime/securesms/database/RemoteKeyRecord.java rename to src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java index 5ab32aca94..71470bf70e 100644 --- a/src/org/thoughtcrime/securesms/database/RemoteKeyRecord.java +++ b/src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java @@ -14,13 +14,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.database; +package org.thoughtcrime.securesms.database.keys; import android.content.Context; import android.util.Log; import org.thoughtcrime.securesms.crypto.InvalidKeyException; import org.thoughtcrime.securesms.crypto.PublicKey; +import org.thoughtcrime.securesms.database.CanonicalAddressDatabase; +import org.thoughtcrime.securesms.database.keys.InvalidKeyIdException; +import org.thoughtcrime.securesms.database.keys.Record; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.Hex; diff --git a/src/org/thoughtcrime/securesms/database/SessionKey.java b/src/org/thoughtcrime/securesms/database/keys/SessionKey.java similarity index 98% rename from src/org/thoughtcrime/securesms/database/SessionKey.java rename to src/org/thoughtcrime/securesms/database/keys/SessionKey.java index 2052bbe8cd..8d2d482f48 100644 --- a/src/org/thoughtcrime/securesms/database/SessionKey.java +++ b/src/org/thoughtcrime/securesms/database/keys/SessionKey.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.database; +package org.thoughtcrime.securesms.database.keys; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; diff --git a/src/org/thoughtcrime/securesms/database/SessionRecord.java b/src/org/thoughtcrime/securesms/database/keys/SessionRecord.java similarity index 96% rename from src/org/thoughtcrime/securesms/database/SessionRecord.java rename to src/org/thoughtcrime/securesms/database/keys/SessionRecord.java index f678aabced..acdc03b235 100644 --- a/src/org/thoughtcrime/securesms/database/SessionRecord.java +++ b/src/org/thoughtcrime/securesms/database/keys/SessionRecord.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.database; +package org.thoughtcrime.securesms.database.keys; import android.content.Context; import android.util.Log; @@ -22,6 +22,9 @@ import android.util.Log; import org.thoughtcrime.securesms.crypto.IdentityKey; import org.thoughtcrime.securesms.crypto.InvalidKeyException; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.CanonicalAddressDatabase; +import org.thoughtcrime.securesms.database.keys.Record; +import org.thoughtcrime.securesms.database.keys.SessionKey; import org.thoughtcrime.securesms.recipients.Recipient; import java.io.FileInputStream; diff --git a/src/org/thoughtcrime/securesms/directory/BloomFilter.java b/src/org/thoughtcrime/securesms/directory/BloomFilter.java new file mode 100644 index 0000000000..edbd05b67a --- /dev/null +++ b/src/org/thoughtcrime/securesms/directory/BloomFilter.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.thoughtcrime.securesms.directory; + +import org.thoughtcrime.securesms.util.Conversions; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +/** + * A simple bloom filter implementation that backs the RedPhone directory. + * + * @author Moxie Marlinspike + * + */ + +public class BloomFilter { + + private final MappedByteBuffer buffer; + private final long length; + private final int hashCount; + + public BloomFilter(File bloomFilter, int hashCount) + throws IOException + { + this.length = bloomFilter.length(); + this.buffer = new FileInputStream(bloomFilter).getChannel() + .map(FileChannel.MapMode.READ_ONLY, 0, length); + this.hashCount = hashCount; + } + + public int getHashCount() { + return hashCount; + } + + private boolean isBitSet(long bitIndex) { + int byteInQuestion = this.buffer.get((int)(bitIndex / 8)); + int bitOffset = (0x01 << (bitIndex % 8)); + + return (byteInQuestion & bitOffset) > 0; + } + + public boolean contains(String entity) { + try { + for (int i=0;i. + */ + +package org.thoughtcrime.securesms.directory; + +import android.content.Context; +import android.util.Log; + +import com.google.thoughtcrimegson.Gson; +import com.google.thoughtcrimegson.JsonParseException; +import com.google.thoughtcrimegson.annotations.SerializedName; +import org.thoughtcrime.securesms.util.PhoneNumberFormatter; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; + +/** + * Handles providing lookups, serializing, and deserializing the RedPhone directory. + * + * @author Moxie Marlinspike + * + */ + +public class NumberFilter { + + private static NumberFilter instance; + + public synchronized static NumberFilter getInstance(Context context) { + if (instance == null) + instance = NumberFilter.deserializeFromFile(context); + + return instance; + } + + private static final String DIRECTORY_META_FILE = "directory.stat"; + + private File bloomFilter; + private String version; + private long capacity; + private int hashCount; + private Context context; + + private NumberFilter(Context context, File bloomFilter, long capacity, + int hashCount, String version) + { + this.context = context.getApplicationContext(); + this.bloomFilter = bloomFilter; + this.capacity = capacity; + this.hashCount = hashCount; + this.version = version; + } + + public synchronized boolean containsNumber(String number) { + try { + if (bloomFilter == null) return false; + else if (number == null || number.length() == 0) return false; + + return new BloomFilter(bloomFilter, hashCount).contains(PhoneNumberFormatter.formatNumber(context, number)); + } catch (IOException ioe) { + Log.w("NumberFilter", ioe); + return false; + } + } + + public synchronized void update(File bloomFilter, long capacity, int hashCount, String version) + { + if (this.bloomFilter != null) + this.bloomFilter.delete(); + + this.bloomFilter = bloomFilter; + this.capacity = capacity; + this.hashCount = hashCount; + this.version = version; + + serializeToFile(context); + } + + private void serializeToFile(Context context) { + if (this.bloomFilter == null) + return; + + try { + FileOutputStream fout = context.openFileOutput(DIRECTORY_META_FILE, 0); + NumberFilterStorage storage = new NumberFilterStorage(bloomFilter.getAbsolutePath(), + capacity, hashCount, version); + + storage.serializeToStream(fout); + fout.close(); + } catch (IOException ioe) { + Log.w("NumberFilter", ioe); + } + } + + private static NumberFilter deserializeFromFile(Context context) { + try { + FileInputStream fis = context.openFileInput(DIRECTORY_META_FILE); + NumberFilterStorage storage = NumberFilterStorage.fromStream(fis); + + if (storage == null) return new NumberFilter(context, null, 0, 0, "0"); + else return new NumberFilter(context, + new File(storage.getDataPath()), + storage.getCapacity(), + storage.getHashCount(), + storage.getVersion()); + } catch (IOException ioe) { + Log.w("NumberFilter", ioe); + return new NumberFilter(context, null, 0, 0, "0"); + } + } + + private static class NumberFilterStorage { + @SerializedName("data_path") + private String dataPath; + + @SerializedName("capacity") + private long capacity; + + @SerializedName("hash_count") + private int hashCount; + + @SerializedName("version") + private String version; + + public NumberFilterStorage(String dataPath, long capacity, int hashCount, String version) { + this.dataPath = dataPath; + this.capacity = capacity; + this.hashCount = hashCount; + this.version = version; + } + + public String getDataPath() { + return dataPath; + } + + public long getCapacity() { + return capacity; + } + + public int getHashCount() { + return hashCount; + } + + public String getVersion() { + return version; + } + + public void serializeToStream(OutputStream out) throws IOException { + out.write(new Gson().toJson(this).getBytes()); + } + + public static NumberFilterStorage fromStream(InputStream in) throws IOException { + try { + return new Gson().fromJson(new BufferedReader(new InputStreamReader(in)), + NumberFilterStorage.class); + } catch (JsonParseException jpe) { + Log.w("NumberFilter", jpe); + throw new IOException("JSON Parse Exception"); + } + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java b/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java index 0cf01e64f9..c4ecc0230e 100644 --- a/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java +++ b/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java @@ -4,10 +4,18 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.util.Log; import com.google.android.gcm.GCMBaseIntentService; +import com.google.thoughtcrimegson.Gson; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.service.RegistrationService; +import org.thoughtcrime.securesms.service.SendReceiveService; +import org.thoughtcrime.securesms.sms.TextMessage; +import org.thoughtcrime.securesms.util.Util; + +import java.io.IOException; +import java.util.ArrayList; public class GcmIntentService extends GCMBaseIntentService { @@ -21,25 +29,52 @@ public class GcmIntentService extends GCMBaseIntentService { intent.putExtra(RegistrationService.GCM_REGISTRATION_ID, registrationId); sendBroadcast(intent); } else { -// -// // Talk to the server directly. -// + try { + getGcmSocket(context).registerGcmId(registrationId); + } catch (IOException e) { + Log.w("GcmIntentService", e); + } } } + @Override + protected void onUnregistered(Context context, String registrationId) { + try { + getGcmSocket(context).unregisterGcmId(registrationId); + } catch (IOException ioe) { + Log.w("GcmIntentService", ioe); + } + } + + @Override protected void onMessage(Context context, Intent intent) { - //To change body of implemented methods use File | Settings | File Templates. + Log.w("GcmIntentService", "Got GCM message!"); + String data = intent.getStringExtra("message"); + Log.w("GcmIntentService", "GCM message: " + data); + + if (Util.isEmpty(data)) + return; + + IncomingGcmMessage message = new Gson().fromJson(data, IncomingGcmMessage.class); + ArrayList messages = new ArrayList(); + messages.add(new TextMessage(message)); + + Intent receivedIntent = new Intent(context, SendReceiveService.class); + receivedIntent.setAction(SendReceiveService.RECEIVE_SMS_ACTION); + receivedIntent.putParcelableArrayListExtra("text_messages", messages); + context.startService(receivedIntent); } @Override protected void onError(Context context, String s) { - //To change body of implemented methods use File | Settings | File Templates. + Log.w("GcmIntentService", "GCM Error: " + s); } - - @Override - protected void onUnregistered(Context context, String s) { - //To change body of implemented methods use File | Settings | File Templates. + private GcmSocket getGcmSocket(Context context) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + String localNumber = preferences.getString(ApplicationPreferencesActivity.LOCAL_NUMBER_PREF, null); + String password = preferences.getString(ApplicationPreferencesActivity.GCM_PASSWORD_PREF, null); + return new GcmSocket(context, localNumber, password); } } diff --git a/src/org/thoughtcrime/securesms/gcm/GcmMessageResponse.java b/src/org/thoughtcrime/securesms/gcm/GcmMessageResponse.java new file mode 100644 index 0000000000..25c1ed787f --- /dev/null +++ b/src/org/thoughtcrime/securesms/gcm/GcmMessageResponse.java @@ -0,0 +1,18 @@ +package org.thoughtcrime.securesms.gcm; + +import java.util.List; + +public class GcmMessageResponse { + private List success; + private List failure; + + public List getSuccess() { + return success; + } + + public List getFailure() { + return failure; + } + + +} diff --git a/src/org/thoughtcrime/securesms/gcm/GcmSender.java b/src/org/thoughtcrime/securesms/gcm/GcmSender.java deleted file mode 100644 index 0bf37ea063..0000000000 --- a/src/org/thoughtcrime/securesms/gcm/GcmSender.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.thoughtcrime.securesms.gcm; - -import android.app.PendingIntent; - -public class GcmSender { - - private static final GcmSender instance = new GcmSender(); - - public static GcmSender getDefault() { - return instance; - } - - public void sendTextMessage(String recipient, String text, - PendingIntent sentIntent, - PendingIntent deliveredIntent) - { - - } - -} diff --git a/src/org/thoughtcrime/securesms/gcm/RegistrationSocket.java b/src/org/thoughtcrime/securesms/gcm/GcmSocket.java similarity index 55% rename from src/org/thoughtcrime/securesms/gcm/RegistrationSocket.java rename to src/org/thoughtcrime/securesms/gcm/GcmSocket.java index 6235abe999..a2df3584e9 100644 --- a/src/org/thoughtcrime/securesms/gcm/RegistrationSocket.java +++ b/src/org/thoughtcrime/securesms/gcm/GcmSocket.java @@ -3,13 +3,18 @@ package org.thoughtcrime.securesms.gcm; import android.content.Context; import android.content.res.AssetManager; import android.util.Base64; +import android.util.Log; import com.google.thoughtcrimegson.Gson; +import org.thoughtcrime.securesms.directory.DirectoryDescriptor; +import org.thoughtcrime.securesms.directory.NumberFilter; import org.thoughtcrime.securesms.util.Util; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -21,18 +26,21 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.util.zip.GZIPInputStream; -public class RegistrationSocket { +public class GcmSocket { private static final String CREATE_ACCOUNT_PATH = "/v1/accounts/%s"; private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/%s"; private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/%s"; + private static final String DIRECTORY_PATH = "/v1/directory/"; + private static final String MESSAGE_PATH = "/v1/messages/"; private final String localNumber; private final String password; private final TrustManagerFactory trustManagerFactory; - public RegistrationSocket(Context context, String localNumber, String password) { + public GcmSocket(Context context, String localNumber, String password) { this.localNumber = localNumber; this.password = password; this.trustManagerFactory = initializeTrustManagerFactory(context); @@ -54,6 +62,118 @@ public class RegistrationSocket { makeRequest(String.format(REGISTER_GCM_PATH, localNumber), "PUT", new Gson().toJson(registration)); } + public void unregisterGcmId(String gcmRegistrationId) throws IOException { + GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId); + makeRequest(String.format(REGISTER_GCM_PATH, localNumber), "DELETE", new Gson().toJson(registration)); + } + + public void sendMessage(String recipient, String messageText) throws IOException { + OutgoingGcmMessage message = new OutgoingGcmMessage(recipient, messageText); + String responseText = makeRequest(MESSAGE_PATH, "POST", new Gson().toJson(message)); + GcmMessageResponse response = new Gson().fromJson(responseText, GcmMessageResponse.class); + + if (response.getFailure().size() != 0) + throw new IOException("Got send failure: " + response.getFailure().get(0)); + } + + public void retrieveDirectory(Context context ) { + try { + DirectoryDescriptor directoryDescriptor = new Gson().fromJson(makeRequest(DIRECTORY_PATH, "GET", null), + DirectoryDescriptor.class); + + File directoryData = downloadExternalFile(context, directoryDescriptor.getUrl()); + + NumberFilter.getInstance(context).update(directoryData, + directoryDescriptor.getCapacity(), + directoryDescriptor.getHashCount(), + directoryDescriptor.getVersion()); + + } catch (IOException ioe) { + Log.w("GcmSocket", ioe); + } + } + + private File downloadExternalFile(Context context, String url) throws IOException { + File download = File.createTempFile("directory", ".dat", context.getFilesDir()); + URL downloadUrl = new URL(url); + HttpsURLConnection connection = (HttpsURLConnection)downloadUrl.openConnection(); + connection.setDoInput(true); + + if (connection.getResponseCode() != 200) { + throw new IOException("Bad response: " + connection.getResponseCode()); + } + + OutputStream output = new FileOutputStream(download); + InputStream input = new GZIPInputStream(connection.getInputStream()); + int read = 0; + byte[] buffer = new byte[4096]; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + + output.close(); + + return download; + } + + private String makeRequest(String urlFragment, String method, String body) throws IOException { + HttpsURLConnection connection = getConnection(urlFragment, method); + + if (body != null) { + connection.setDoOutput(true); + } + + connection.connect(); + + if (body != null) { + Log.w("GcmSocket", method + " -- " + body); + OutputStream out = connection.getOutputStream(); + out.write(body.getBytes()); + out.close(); + } + + if (connection.getResponseCode() != 200) { + throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage()); + } + + return Util.readFully(connection.getInputStream()); + } + + private HttpsURLConnection getConnection(String urlFragment, String method) throws IOException { + try { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, trustManagerFactory.getTrustManagers(), null); + + URL url = new URL(String.format("https://gcm.textsecure.whispersystems.org%s", urlFragment)); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setSSLSocketFactory(context.getSocketFactory()); + connection.setRequestMethod(method); + connection.setRequestProperty("Content-Type", "application/json"); + + if (password != null) { + System.out.println("Adding authorization header: " + getAuthorizationHeader()); + connection.setRequestProperty("Authorization", getAuthorizationHeader()); + } + + return connection; + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } catch (KeyManagementException e) { + throw new AssertionError(e); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + } + + private String getAuthorizationHeader() { + try { + return "Basic " + new String(Base64.encode((localNumber + ":" + password).getBytes("UTF-8"), Base64.NO_WRAP)); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + private TrustManagerFactory initializeTrustManagerFactory(Context context) { try { AssetManager assetManager = context.getAssets(); @@ -77,54 +197,6 @@ public class RegistrationSocket { } } - private String makeRequest(String urlFragment, String method, String body) throws IOException { - try { - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, trustManagerFactory.getTrustManagers(), null); - - URL url = new URL(String.format("https://gcm.textsecure.whispersystems.org/%s", urlFragment)); - HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); - connection.setSSLSocketFactory(context.getSocketFactory()); - connection.setRequestMethod(method); - connection.setRequestProperty("Content-Type", "application/json"); - - if (password != null) { - connection.setRequestProperty("Authorization", getAuthorizationHeader()); - } - - if (body != null) { - connection.setDoOutput(true); - } - - connection.connect(); - - if (body != null) { - OutputStream out = connection.getOutputStream(); - out.write(body.getBytes()); - out.close(); - } - - if (connection.getResponseCode() != 200) { - throw new IOException("Bad response: " + connection.getResponseCode()); - } - - return Util.readFully(connection.getInputStream()); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (KeyManagementException e) { - throw new AssertionError(e); - } catch (MalformedURLException e) { - throw new AssertionError(e); - } - } - - private String getAuthorizationHeader() { - try { - return "Basic " + new String(Base64.encode((localNumber + ":" + password).getBytes("UTF-8"), Base64.NO_WRAP)); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } - } private class Verification { diff --git a/src/org/thoughtcrime/securesms/gcm/IncomingGcmMessage.java b/src/org/thoughtcrime/securesms/gcm/IncomingGcmMessage.java new file mode 100644 index 0000000000..9cc40bcdd0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/gcm/IncomingGcmMessage.java @@ -0,0 +1,42 @@ +package org.thoughtcrime.securesms.gcm; + +import java.util.LinkedList; +import java.util.List; + +public class IncomingGcmMessage { + + private String source; + private List destinations; + private String messageText; + private List attachments; + private long timestamp; + + public IncomingGcmMessage(String source, List destinations, String messageText, List attachments, long timestamp) { + this.source = source; + this.destinations = destinations; + this.messageText = messageText; + this.attachments = attachments; + this.timestamp = timestamp; + } + + public long getTimestampMillis() { + return timestamp; + } + + public String getSource() { + return source; + } + + public List getAttachments() { + return attachments; + } + + public String getMessageText() { + return messageText; + } + + public List getDestinations() { + return destinations; + } + +} diff --git a/src/org/thoughtcrime/securesms/gcm/OptimizingTransport.java b/src/org/thoughtcrime/securesms/gcm/OptimizingTransport.java new file mode 100644 index 0000000000..10a8f8be46 --- /dev/null +++ b/src/org/thoughtcrime/securesms/gcm/OptimizingTransport.java @@ -0,0 +1,79 @@ +package org.thoughtcrime.securesms.gcm; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.telephony.SmsManager; +import android.util.Log; + +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; +import org.thoughtcrime.securesms.directory.NumberFilter; +import org.thoughtcrime.securesms.util.PhoneNumberFormatter; + +import java.io.IOException; +import java.util.ArrayList; + +public class OptimizingTransport { + + public static void sendTextMessage(Context context, String destinationAddress, String message, + PendingIntent sentIntent, PendingIntent deliveredIntent) + { + Log.w("OptimzingTransport", "Outgoing message: " + PhoneNumberFormatter.formatNumber(context, destinationAddress)); + NumberFilter filter = NumberFilter.getInstance(context); + + if (filter.containsNumber(PhoneNumberFormatter.formatNumber(context, destinationAddress))) { + Log.w("OptimzingTransport", "In the filter, sending GCM..."); + sendGcmTextMessage(context, destinationAddress, message, sentIntent, deliveredIntent); + } else { + Log.w("OptimzingTransport", "Not in the filter, sending SMS..."); + sendSmsTextMessage(destinationAddress, message, sentIntent, deliveredIntent); + } + } + + public static void sendMultipartTextMessage(Context context, + String recipient, + ArrayList messages, + ArrayList sentIntents, + ArrayList deliveredIntents) + { + // FIXME + + sendTextMessage(context, recipient, messages.get(0), sentIntents.get(0), deliveredIntents == null ? null : deliveredIntents.get(0)); + } + + + private static void sendGcmTextMessage(Context context, String recipient, String messageText, + PendingIntent sentIntent, PendingIntent deliveredIntent) + { + try { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + String localNumber = preferences.getString(ApplicationPreferencesActivity.LOCAL_NUMBER_PREF, null); + String password = preferences.getString(ApplicationPreferencesActivity.GCM_PASSWORD_PREF, null); + + if (localNumber == null || password == null) { + Log.w("OptimzingTransport", "No credentials, falling back to SMS..."); + sendSmsTextMessage(recipient, messageText, sentIntent, deliveredIntent); + return; + } + + GcmSocket gcmSocket = new GcmSocket(context, localNumber, password); + gcmSocket.sendMessage(PhoneNumberFormatter.formatNumber(context, recipient), messageText); + sentIntent.send(Activity.RESULT_OK); + } catch (IOException ioe) { + Log.w("OptimizingTransport", ioe); + Log.w("OptimzingTransport", "IOException, falling back to SMS..."); + sendSmsTextMessage(recipient, messageText, sentIntent, deliveredIntent); + } catch (PendingIntent.CanceledException e) { + Log.w("OptimizingTransport", e); + } + } + + private static void sendSmsTextMessage(String recipient, String message, + PendingIntent sentIntent, PendingIntent deliveredIntent) + { + SmsManager.getDefault().sendTextMessage(recipient, null, message, sentIntent, deliveredIntent); + } + +} diff --git a/src/org/thoughtcrime/securesms/gcm/OutgoingGcmMessage.java b/src/org/thoughtcrime/securesms/gcm/OutgoingGcmMessage.java new file mode 100644 index 0000000000..911222f1c0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/gcm/OutgoingGcmMessage.java @@ -0,0 +1,37 @@ +package org.thoughtcrime.securesms.gcm; + +import java.util.LinkedList; +import java.util.List; + +public class OutgoingGcmMessage { + + private List destinations; + private String messageText; + private List attachments; + + public OutgoingGcmMessage(List destinations, String messageText, List attachments) { + this.destinations = destinations; + this.messageText = messageText; + this.attachments = attachments; + } + + public OutgoingGcmMessage(String destination, String messageText) { + this.destinations = new LinkedList(); + this.destinations.add(destination); + this.messageText = messageText; + this.attachments = new LinkedList(); + } + + public List getDestinations() { + return destinations; + } + + public String getMessageText() { + return messageText; + } + + public List getAttachments() { + return attachments; + } + +} diff --git a/src/org/thoughtcrime/securesms/service/RegistrationService.java b/src/org/thoughtcrime/securesms/service/RegistrationService.java index 8e463ba52f..afd94b3fc7 100644 --- a/src/org/thoughtcrime/securesms/service/RegistrationService.java +++ b/src/org/thoughtcrime/securesms/service/RegistrationService.java @@ -17,7 +17,7 @@ import com.google.android.gcm.GCMRegistrar; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.gcm.GcmIntentService; import org.thoughtcrime.securesms.gcm.GcmRegistrationTimeoutException; -import org.thoughtcrime.securesms.gcm.RegistrationSocket; +import org.thoughtcrime.securesms.gcm.GcmSocket; import org.thoughtcrime.securesms.util.Util; import java.io.IOException; @@ -127,14 +127,14 @@ public class RegistrationService extends Service { registerReceiver(gcmRegistrationReceiver, filter); } - private void shutdownChallengeListener() { + private synchronized void shutdownChallengeListener() { if (challengeReceiver != null) { unregisterReceiver(challengeReceiver); challengeReceiver = null; } } - private void shutdownGcmRegistrationListener() { + private synchronized void shutdownGcmRegistrationListener() { if (gcmRegistrationReceiver != null) { unregisterReceiver(gcmRegistrationReceiver); gcmRegistrationReceiver = null; @@ -144,7 +144,7 @@ public class RegistrationService extends Service { private void handleRegistrationIntent(Intent intent) { markAsVerifying(true); - RegistrationSocket socket; + GcmSocket socket; String number = intent.getStringExtra("e164number"); try { @@ -153,7 +153,7 @@ public class RegistrationService extends Service { initializeGcmRegistrationListener(); setState(new RegistrationState(RegistrationState.STATE_CONNECTING, number)); - socket = new RegistrationSocket(this, number, password); + socket = new GcmSocket(this, number, password); socket.createAccount(); setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number)); @@ -165,6 +165,9 @@ public class RegistrationService extends Service { String gcmRegistrationId = waitForGcmRegistrationId(); socket.registerGcmId(gcmRegistrationId); + setState(new RegistrationState(RegistrationState.STATE_RETRIEVING_DIRECTORY, number)); + socket.retrieveDirectory(this); + markAsVerified(number, password); setState(new RegistrationState(RegistrationState.STATE_COMPLETE, number)); @@ -315,6 +318,8 @@ public class RegistrationService extends Service { public static final int STATE_GCM_REGISTERING = 9; public static final int STATE_GCM_TIMEOUT = 10; + public static final int STATE_RETRIEVING_DIRECTORY = 11; + public final int state; public final String number; diff --git a/src/org/thoughtcrime/securesms/service/SendReceiveService.java b/src/org/thoughtcrime/securesms/service/SendReceiveService.java index a4891fdf70..ffbcb41cfe 100644 --- a/src/org/thoughtcrime/securesms/service/SendReceiveService.java +++ b/src/org/thoughtcrime/securesms/service/SendReceiveService.java @@ -63,11 +63,11 @@ public class SendReceiveService extends Service { private ToastHandler toastHandler; - private SmsReceiver smsReceiver; - private SmsSender smsSender; - private MmsReceiver mmsReceiver; - private MmsSender mmsSender; - private MmsDownloader mmsDownloader; + private SmsReceiver smsReceiver; + private SmsSender smsSender; + private MmsReceiver mmsReceiver; + private MmsSender mmsSender; + private MmsDownloader mmsDownloader; private MasterSecret masterSecret; private boolean hasSecret; diff --git a/src/org/thoughtcrime/securesms/service/SmsListener.java b/src/org/thoughtcrime/securesms/service/SmsListener.java index 2648d3a1e7..4e720da3a8 100644 --- a/src/org/thoughtcrime/securesms/service/SmsListener.java +++ b/src/org/thoughtcrime/securesms/service/SmsListener.java @@ -27,6 +27,9 @@ import android.util.Log; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.protocol.WirePrefix; +import org.thoughtcrime.securesms.sms.TextMessage; + +import java.util.ArrayList; public class SmsListener extends BroadcastReceiver { @@ -74,6 +77,16 @@ public class SmsListener extends BroadcastReceiver { return bodyBuilder.toString(); } + private ArrayList getAsTextMessages(Intent intent) { + Object[] pdus = (Object[])intent.getExtras().get("pdus"); + ArrayList messages = new ArrayList(pdus.length); + + for (int i=0;i messagesList = intent.getExtras().getParcelableArrayList("text_messages"); + TextMessage[] messages = messagesList.toArray(new TextMessage[0]); +// Bundle bundle = intent.getExtras(); +// SmsMessage[] messages = parseMessages(bundle); String message = assembleMessageFragments(messages); if (message != null) { diff --git a/src/org/thoughtcrime/securesms/service/SmsSender.java b/src/org/thoughtcrime/securesms/service/SmsSender.java index c2e3071ca7..fbe6ef1fd9 100644 --- a/src/org/thoughtcrime/securesms/service/SmsSender.java +++ b/src/org/thoughtcrime/securesms/service/SmsSender.java @@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.SessionCipher; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.gcm.OptimizingTransport; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.protocol.KeyExchangeWirePrefix; import org.thoughtcrime.securesms.protocol.Prefix; @@ -224,7 +225,9 @@ public class SmsSender { // the message as a failure. That way at least it doesn't repeatedly crash every time you start // the app. try { - SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents); + OptimizingTransport.sendMultipartTextMessage(context, recipient, messages, sentIntents, deliveredIntents); +// +// SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents); } catch (NullPointerException npe) { Log.w("SmsSender", npe); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); @@ -257,8 +260,10 @@ public class SmsSender { // the message as a failure. That way at least it doesn't repeatedly crash every time you start // the app. try { - SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i), sentIntents.get(i), - deliveredIntents == null ? null : deliveredIntents.get(i)); + OptimizingTransport.sendTextMessage(context, recipient, messages.get(i), sentIntents.get(i), + deliveredIntents == null ? null : deliveredIntents.get(i)); +// SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i), sentIntents.get(i), +// deliveredIntents == null ? null : deliveredIntents.get(i)); } catch (NullPointerException npe) { Log.w("SmsSender", npe); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); diff --git a/src/org/thoughtcrime/securesms/sms/TextMessage.java b/src/org/thoughtcrime/securesms/sms/TextMessage.java new file mode 100644 index 0000000000..11a5cba534 --- /dev/null +++ b/src/org/thoughtcrime/securesms/sms/TextMessage.java @@ -0,0 +1,104 @@ +package org.thoughtcrime.securesms.sms; + +import android.os.Parcel; +import android.os.Parcelable; +import android.telephony.SmsMessage; + +import org.thoughtcrime.securesms.gcm.IncomingGcmMessage; + +public class TextMessage implements Parcelable { + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public TextMessage createFromParcel(Parcel in) { + return new TextMessage(in); + } + + @Override + public TextMessage[] newArray(int size) { + return new TextMessage[size]; + } + }; + + private final String message; + private final String sender; + private final int protocol; + private final String serviceCenterAddress; + private final boolean replyPathPresent; + private final String pseudoSubject; + private final long sentTimestampMillis; + + public TextMessage(SmsMessage message) { + this.message = message.getDisplayMessageBody(); + this.sender = message.getDisplayOriginatingAddress(); + this.protocol = message.getProtocolIdentifier(); + this.serviceCenterAddress = message.getServiceCenterAddress(); + this.replyPathPresent = message.isReplyPathPresent(); + this.pseudoSubject = message.getPseudoSubject(); + this.sentTimestampMillis = message.getTimestampMillis(); + } + + public TextMessage(IncomingGcmMessage message) { + this.message = message.getMessageText(); + this.sender = message.getSource(); + this.protocol = 31337; + this.serviceCenterAddress = "GCM"; + this.replyPathPresent = true; + this.pseudoSubject = ""; + this.sentTimestampMillis = message.getTimestampMillis(); + } + + public TextMessage(Parcel in) { + this.message = in.readString(); + this.sender = in.readString(); + this.protocol = in.readInt(); + this.serviceCenterAddress = in.readString(); + this.replyPathPresent = (in.readInt() == 1); + this.pseudoSubject = in.readString(); + this.sentTimestampMillis = in.readLong(); + } + + public long getSentTimestampMillis() { + return sentTimestampMillis; + } + + public String getPseudoSubject() { + return pseudoSubject; + } + + public String getMessage() { + return message; + } + + public String getSender() { + return sender; + } + + public int getProtocol() { + return protocol; + } + + public String getServiceCenterAddress() { + return serviceCenterAddress; + } + + public boolean isReplyPathPresent() { + return replyPathPresent; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(message); + out.writeString(sender); + out.writeInt(protocol); + out.writeString(serviceCenterAddress); + out.writeInt(replyPathPresent ? 1 : 0); + out.writeString(pseudoSubject); + out.writeLong(sentTimestampMillis); + } +} diff --git a/src/org/thoughtcrime/securesms/util/Conversions.java b/src/org/thoughtcrime/securesms/util/Conversions.java index 91aede2eb8..561bf73ca5 100644 --- a/src/org/thoughtcrime/securesms/util/Conversions.java +++ b/src/org/thoughtcrime/securesms/util/Conversions.java @@ -158,6 +158,14 @@ public class Conversions { return byteArrayToLong(bytes, 0); } + public static long byteArray4ToLong(byte[] bytes, int offset) { + return + ((bytes[offset + 0] & 0xffL) << 24) | + ((bytes[offset + 1] & 0xffL) << 16) | + ((bytes[offset + 2] & 0xffL) << 8) | + ((bytes[offset + 3] & 0xffL)); + } + public static long byteArrayToLong(byte[] bytes, int offset) { return ((bytes[offset] & 0xffL) << 56) |