Beginning of libtextsecure refactor.

1) Break out appropriate components.

2) Switch the incoming pipeline from SendReceiveService to
   the JobManager.
This commit is contained in:
Moxie Marlinspike
2014-11-12 15:21:32 -08:00
parent 4cab657ebe
commit a3f1d9cdfd
152 changed files with 3521 additions and 3280 deletions
@@ -20,11 +20,16 @@ import android.app.Application;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.PRNGFixes;
import org.thoughtcrime.securesms.jobs.EncryptingJobSerializer;
import org.thoughtcrime.securesms.jobs.persistence.EncryptingJobSerializer;
import org.thoughtcrime.securesms.jobs.GcmRefreshJob;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirementProvider;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.jobqueue.requirements.NetworkRequirementProvider;
import org.whispersystems.jobqueue.requirements.RequirementProvider;
import java.util.LinkedList;
import java.util.List;
/**
* Will be called once when the TextSecure process is created.
@@ -58,8 +63,12 @@ public class ApplicationContext extends Application {
}
private void initializeJobManager() {
this.jobManager = new JobManager(this, "TextSecureJobs",
new NetworkRequirementProvider(this),
List<RequirementProvider> providers = new LinkedList<RequirementProvider>() {{
add(new NetworkRequirementProvider(ApplicationContext.this));
add(new MasterSecretRequirementProvider(ApplicationContext.this));
}};
this.jobManager = new JobManager(this, "TextSecureJobs", providers,
new EncryptingJobSerializer(this), 5);
}
@@ -64,7 +64,7 @@ import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Trimmer;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.push.exceptions.AuthorizationFailedException;
import org.whispersystems.textsecure.push.PushServiceSocket;
@@ -31,9 +31,9 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
/**
* Activity which prompts the user to initiate a secure
@@ -64,7 +64,7 @@ import org.thoughtcrime.securesms.components.EmojiToggle;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
@@ -104,10 +104,10 @@ import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
@@ -818,7 +818,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
};
registerReceiver(securityUpdateReceiver,
new IntentFilter(KeyExchangeProcessor.SECURITY_UPDATE_EVENT),
new IntentFilter(SecurityEvent.SECURITY_UPDATE_EVENT),
KeyCachingService.KEY_PERMISSION, null);
registerReceiver(groupUpdateReceiver,
@@ -25,7 +25,7 @@ import android.view.ViewGroup;
import android.widget.AbsListView;
import android.support.v4.widget.CursorAdapter;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
@@ -38,9 +38,17 @@ import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.FutureTaskListener;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.util.FutureTaskListener;
import org.whispersystems.textsecure.util.Util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.sql.Date;
import java.text.SimpleDateFormat;
@@ -38,12 +38,14 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -51,7 +53,6 @@ import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.Emoji;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.FutureTaskListener;
import org.whispersystems.textsecure.util.ListenableFutureTask;
@@ -490,13 +491,10 @@ public class ConversationItem extends LinearLayout {
mmsDownloadButton.setVisibility(View.GONE);
mmsDownloadingLabel.setVisibility(View.VISIBLE);
Intent intent = new Intent(context, SendReceiveService.class);
intent.putExtra("content_location", new String(notificationRecord.getContentLocation()));
intent.putExtra("message_id", notificationRecord.getId());
intent.putExtra("transaction_id", notificationRecord.getTransactionId());
intent.putExtra("thread_id", notificationRecord.getThreadId());
intent.setAction(SendReceiveService.DOWNLOAD_MMS_ACTION);
context.startService(intent);
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MmsDownloadJob(context, messageRecord.getId(),
messageRecord.getThreadId(), false));
}
}
@@ -49,7 +49,7 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class ConversationListActivity extends PassphraseRequiredActionBarActivity
@@ -24,8 +24,8 @@ import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.CursorAdapter;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
@@ -51,7 +51,7 @@ import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Dialogs;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.util.Set;
@@ -27,14 +27,18 @@ import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.SmsDecryptJob;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.util.ParcelUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.whispersystems.jobqueue.EncryptionKeys;
import java.io.File;
import java.util.SortedSet;
@@ -49,6 +53,7 @@ public class DatabaseUpgradeActivity extends Activity {
public static final int ASYMMETRIC_MASTER_SECRET_FIX_VERSION = 73;
public static final int NO_V1_VERSION = 83;
public static final int SIGNED_PREKEY_VERSION = 83;
public static final int NO_DECRYPT_QUEUE_VERSION = 84;
private static final SortedSet<Integer> UPGRADE_VERSIONS = new TreeSet<Integer>() {{
add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION);
@@ -57,6 +62,7 @@ public class DatabaseUpgradeActivity extends Activity {
add(ASYMMETRIC_MASTER_SECRET_FIX_VERSION);
add(NO_V1_VERSION);
add(SIGNED_PREKEY_VERSION);
add(NO_DECRYPT_QUEUE_VERSION);
}};
private MasterSecret masterSecret;
@@ -77,7 +83,10 @@ public class DatabaseUpgradeActivity extends Activity {
.execute(VersionTracker.getLastSeenVersion(this));
} else {
VersionTracker.updateLastSeenVersion(this);
DecryptingQueue.schedulePendingDecrypts(DatabaseUpgradeActivity.this, masterSecret);
ApplicationContext.getInstance(this)
.getJobManager()
.setEncryptionKeys(new EncryptionKeys(ParcelUtil.serialize(masterSecret)));
// DecryptingQueue.schedulePendingDecrypts(DatabaseUpgradeActivity.this, masterSecret);
MessageNotifier.updateNotification(DatabaseUpgradeActivity.this, masterSecret);
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
finish();
@@ -165,6 +174,25 @@ public class DatabaseUpgradeActivity extends Activity {
.add(new CreateSignedPreKeyJob(context, masterSecret));
}
if (params[0] < NO_DECRYPT_QUEUE_VERSION) {
SmsDatabase.Reader reader = null;
SmsMessageRecord record;
try {
reader = DatabaseFactory.getEncryptingSmsDatabase(getApplicationContext())
.getDecryptInProgressMessages(masterSecret);
while ((record = reader.getNext()) != null) {
ApplicationContext.getInstance(getApplicationContext())
.getJobManager()
.add(new SmsDecryptJob(getApplicationContext(), record.getId()));
}
} finally {
if (reader != null)
reader.close();
}
}
return null;
}
@@ -180,7 +208,11 @@ public class DatabaseUpgradeActivity extends Activity {
@Override
protected void onPostExecute(Void result) {
VersionTracker.updateLastSeenVersion(DatabaseUpgradeActivity.this);
DecryptingQueue.schedulePendingDecrypts(DatabaseUpgradeActivity.this, masterSecret);
// DecryptingQueue.schedulePendingDecrypts(DatabaseUpgradeActivity.this, masterSecret);
ApplicationContext.getInstance(DatabaseUpgradeActivity.this)
.getJobManager()
.setEncryptionKeys(new EncryptionKeys(ParcelUtil.serialize(masterSecret)));
MessageNotifier.updateNotification(DatabaseUpgradeActivity.this, masterSecret);
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
@@ -14,11 +14,10 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.Dialogs;
//import org.thoughtcrime.securesms.database.EncryptedBackupExporter;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.database.PlaintextBackupExporter;
import org.thoughtcrime.securesms.util.Dialogs;
import java.io.IOException;
@@ -27,7 +27,6 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.util.Pair;
@@ -62,7 +61,7 @@ import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.util.InvalidNumberException;
@@ -9,7 +9,7 @@ import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.view.MenuItem;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class ImportExportActivity extends PassphraseRequiredActionBarActivity {
@@ -14,14 +14,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.database.EncryptedBackupExporter;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptedBackupExporter;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.database.PlaintextBackupImporter;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.Dialogs;
import java.io.IOException;
@@ -33,6 +33,7 @@ import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.providers.PartProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -40,7 +41,6 @@ import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.whispersystems.textsecure.crypto.MasterSecret;
import java.io.IOException;
import java.io.InputStream;
@@ -16,18 +16,16 @@
*/
package org.thoughtcrime.securesms;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.MenuItem;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.whispersystems.textsecure.crypto.MasterSecret;
public class MmsPreferencesActivity extends PassphraseRequiredActionBarActivity {
@@ -72,7 +70,6 @@ public class MmsPreferencesActivity extends PassphraseRequiredActionBarActivity
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
handleDownloadMmsPendingApn();
finish();
return true;
}
@@ -82,13 +79,7 @@ public class MmsPreferencesActivity extends PassphraseRequiredActionBarActivity
@Override
public void onBackPressed() {
handleDownloadMmsPendingApn();
super.onBackPressed();
}
private void handleDownloadMmsPendingApn() {
Intent intent = new Intent(this, SendReceiveService.class);
intent.setAction(SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION);
startService(intent);
}
}
@@ -35,7 +35,7 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -23,7 +23,7 @@ import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.v7.app.ActionBarActivity;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.MemoryCleaner;
@@ -53,7 +53,7 @@ public abstract class PassphraseActivity extends ActionBarActivity {
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService();
keyCachingService = ((KeyCachingService.KeySetBinder)service).getService();
keyCachingService.setMasterSecret(masterSecret);
PassphraseActivity.this.unbindService(PassphraseActivity.this.serviceConnection);
@@ -26,7 +26,7 @@ import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -23,10 +23,10 @@ import android.os.Bundle;
import android.support.v7.app.ActionBar;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.whispersystems.textsecure.util.Util;
@@ -42,7 +42,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.util.Util;
/**
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class PassphraseRequiredActionBarActivity extends ActionBarActivity implements PassphraseRequiredActivity {
@@ -1,6 +1,6 @@
package org.thoughtcrime.securesms;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public interface PassphraseRequiredActivity {
public void onMasterSecretCleared();
@@ -2,23 +2,19 @@ package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.IBinder;
import android.view.WindowManager;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class PassphraseRequiredMixin {
private KeyCachingServiceConnection serviceConnection;
private BroadcastReceiver clearKeyReceiver;
private BroadcastReceiver newKeyReceiver;
@@ -29,13 +25,12 @@ public class PassphraseRequiredMixin {
public <T extends Activity & PassphraseRequiredActivity> void onResume(T activity) {
initializeScreenshotSecurity(activity);
initializeNewKeyReceiver(activity);
initializeServiceConnection(activity);
initializeFromMasterSecret(activity);
KeyCachingService.registerPassphraseActivityStarted(activity);
}
public <T extends Activity & PassphraseRequiredActivity> void onPause(T activity) {
removeNewKeyReceiver(activity);
removeServiceConnection(activity);
KeyCachingService.registerPassphraseActivityStopped(activity);
}
@@ -78,14 +73,14 @@ public class PassphraseRequiredMixin {
activity.registerReceiver(newKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
}
private <T extends Activity & PassphraseRequiredActivity> void initializeServiceConnection(T activity) {
Intent cachingIntent = new Intent(activity, KeyCachingService.class);
activity.startService(cachingIntent);
private <T extends Activity & PassphraseRequiredActivity> void initializeFromMasterSecret(T activity) {
MasterSecret masterSecret = KeyCachingService.getMasterSecret(activity);
this.serviceConnection = new KeyCachingServiceConnection(activity);
Intent bindIntent = new Intent(activity, KeyCachingService.class);
activity.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE);
if (masterSecret == null) {
activity.onMasterSecretCleared();
} else {
activity.onNewMasterSecret(masterSecret);
}
}
private void removeClearKeyReceiver(Context context) {
@@ -101,36 +96,4 @@ public class PassphraseRequiredMixin {
newKeyReceiver = null;
}
}
private void removeServiceConnection(Context context) {
if (this.serviceConnection != null) {
context.unbindService(this.serviceConnection);
}
}
private static class KeyCachingServiceConnection implements ServiceConnection {
private final PassphraseRequiredActivity activity;
public KeyCachingServiceConnection(PassphraseRequiredActivity activity) {
this.activity = activity;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
KeyCachingService keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService();
MasterSecret masterSecret = keyCachingService.getMasterSecret();
if (masterSecret == null) {
activity.onMasterSecretCleared();
} else {
activity.onNewMasterSecret(masterSecret);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}
}
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class PassphraseRequiredSherlockListActivity extends ActionBarListActivity implements PassphraseRequiredActivity {
@@ -30,48 +30,34 @@ import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.TextSecureCipher;
import org.thoughtcrime.securesms.crypto.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.jobs.SmsDecryptJob;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.StaleKeyExchangeException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.state.IdentityKeyStore;
import org.whispersystems.textsecure.crypto.IdentityKeyParcelable;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.TransportDetails;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.textsecure.push.PushTransportDetails;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException;
import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal.Type;
/**
* Activity for displaying sent/received session keys.
*
@@ -87,13 +73,11 @@ public class ReceiveKeyActivity extends Activity {
private Recipient recipient;
private int recipientDeviceId;
private long threadId;
private long messageId;
private MasterSecret masterSecret;
private PreKeyWhisperMessage keyExchangeMessageBundle;
private KeyExchangeMessage keyExchangeMessage;
private IdentityKey identityUpdateMessage;
private MasterSecret masterSecret;
private IncomingKeyExchangeMessage message;
private IdentityKey identityKey;
@Override
protected void onCreate(Bundle state) {
@@ -118,7 +102,7 @@ public class ReceiveKeyActivity extends Activity {
}
private void initializeText() {
if (isTrusted(keyExchangeMessage, keyExchangeMessageBundle, identityUpdateMessage)) {
if (isTrusted(this.identityKey)) {
initializeTrustedText();
} else {
initializeUntrustedText();
@@ -135,16 +119,10 @@ public class ReceiveKeyActivity extends Activity {
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
IdentityKey remoteIdentity;
if (identityUpdateMessage != null) remoteIdentity = identityUpdateMessage;
else if (keyExchangeMessageBundle != null) remoteIdentity = keyExchangeMessageBundle.getIdentityKey();
else remoteIdentity = keyExchangeMessage.getIdentityKey();
Intent intent = new Intent(ReceiveKeyActivity.this, VerifyIdentityActivity.class);
intent.putExtra("recipient", recipient);
intent.putExtra("master_secret", masterSecret);
intent.putExtra("remote_identity", new IdentityKeyParcelable(remoteIdentity));
intent.putExtra("remote_identity", new IdentityKeyParcelable(identityKey));
startActivity(intent);
}
}, getString(R.string.ReceiveKeyActivity_the_signature_on_this_key_exchange_is_different).length() +1,
@@ -154,43 +132,32 @@ public class ReceiveKeyActivity extends Activity {
descriptionText.setMovementMethod(LinkMovementMethod.getInstance());
}
private boolean isTrusted(KeyExchangeMessage message, PreKeyWhisperMessage messageBundle, IdentityKey identityUpdateMessage) {
private boolean isTrusted(IdentityKey identityKey) {
long recipientId = recipient.getRecipientId();
IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(this, masterSecret);
if (message != null) return identityKeyStore.isTrustedIdentity(recipientId, message.getIdentityKey());
else if (messageBundle != null) return identityKeyStore.isTrustedIdentity(recipientId, messageBundle.getIdentityKey());
else if (identityUpdateMessage != null) return identityKeyStore.isTrustedIdentity(recipientId, identityUpdateMessage);
return false;
return identityKeyStore.isTrustedIdentity(recipientId, identityKey);
}
private void initializeKey()
throws InvalidKeyException, InvalidVersionException,
InvalidMessageException, LegacyMessageException
InvalidMessageException, LegacyMessageException
{
try {
String messageBody = getIntent().getStringExtra("body");
IncomingTextMessage message = new IncomingTextMessage(recipient.getNumber(),
recipientDeviceId,
System.currentTimeMillis(),
getIntent().getStringExtra("body"),
Optional.<TextSecureGroup>absent());
if (getIntent().getBooleanExtra("is_bundle", false)) {
boolean isPush = getIntent().getBooleanExtra("is_push", false);
byte[] body;
if (isPush) {
body = Base64.decode(messageBody.getBytes());
} else {
body = new SmsTransportDetails().getDecodedMessage(messageBody.getBytes());
}
this.keyExchangeMessageBundle = new PreKeyWhisperMessage(body);
} else if (getIntent().getBooleanExtra("is_identity_update", false)) {
this.identityUpdateMessage = new IdentityKey(Base64.decodeWithoutPadding(messageBody), 0);
} else {
this.keyExchangeMessage = new KeyExchangeMessage(Base64.decodeWithoutPadding(messageBody));
}
} catch (IOException e) {
throw new AssertionError(e);
if (getIntent().getBooleanExtra("is_bundle", false)) {
this.message = new IncomingPreKeyBundleMessage(message, message.getMessageBody());
} else if (getIntent().getBooleanExtra("is_identity_update", false)) {
this.message = new IncomingIdentityUpdateMessage(message, message.getMessageBody());
} else {
this.message = new IncomingKeyExchangeMessage(message, message.getMessageBody());
}
this.identityKey = getIdentityKey(this.message);
}
private void initializeResources() {
@@ -199,7 +166,6 @@ public class ReceiveKeyActivity extends Activity {
this.cancelButton = (Button) findViewById(R.id.cancel_button);
this.recipient = getIntent().getParcelableExtra("recipient");
this.recipientDeviceId = getIntent().getIntExtra("recipient_device_id", -1);
this.threadId = getIntent().getLongExtra("thread_id", -1);
this.messageId = getIntent().getLongExtra("message_id", -1);
this.masterSecret = getIntent().getParcelableExtra("master_secret");
}
@@ -209,6 +175,26 @@ public class ReceiveKeyActivity extends Activity {
this.cancelButton.setOnClickListener(new CancelListener());
}
private IdentityKey getIdentityKey(IncomingKeyExchangeMessage message)
throws InvalidKeyException, InvalidVersionException,
InvalidMessageException, LegacyMessageException
{
try {
if (message.isIdentityUpdate()) {
return new IdentityKey(Base64.decodeWithoutPadding(message.getMessageBody()), 0);
} else if (message.isPreKeyBundle()) {
boolean isPush = getIntent().getBooleanExtra("is_push", false);
if (isPush) return new PreKeyWhisperMessage(Base64.decode(message.getMessageBody())).getIdentityKey();
else return new PreKeyWhisperMessage(Base64.decodeWithoutPadding(message.getMessageBody())).getIdentityKey();
} else {
return new KeyExchangeMessage(Base64.decodeWithoutPadding(message.getMessageBody())).getIdentityKey();
}
} catch (IOException e) {
throw new AssertionError(e);
}
}
private class OkListener implements View.OnClickListener {
@Override
public void onClick(View v) {
@@ -225,75 +211,20 @@ public class ReceiveKeyActivity extends Activity {
@Override
protected Void doInBackground(Void... params) {
if (keyExchangeMessage != null) {
try {
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(),
recipientDeviceId);
KeyExchangeProcessor processor = new KeyExchangeProcessor(ReceiveKeyActivity.this,
masterSecret, recipientDevice);
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ReceiveKeyActivity.this);
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this);
Context context = ReceiveKeyActivity.this;
IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(ReceiveKeyActivity.this,
masterSecret);
identityKeyStore.saveIdentity(recipient.getRecipientId(), keyExchangeMessage.getIdentityKey());
identityDatabase.saveIdentity(masterSecret, recipient.getRecipientId(), identityKey);
processor.processKeyExchangeMessage(keyExchangeMessage, threadId);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsProcessedKeyExchange(messageId);
} catch (InvalidKeyException e) {
Log.w("ReceiveKeyActivity", e);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsCorruptKeyExchange(messageId);
} catch (StaleKeyExchangeException e) {
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsStaleKeyExchange(messageId);
} catch (UntrustedIdentityException e) {
throw new AssertionError(e);
}
} else if (keyExchangeMessageBundle != null) {
try {
Context context = ReceiveKeyActivity.this;
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId);
TransportDetails transportDetails = getIntent().getBooleanExtra("is_push", false) ?
new PushTransportDetails(keyExchangeMessageBundle.getMessageVersion()) :
new SmsTransportDetails();
TextSecureCipher cipher = new TextSecureCipher(ReceiveKeyActivity.this, masterSecret, recipientDevice, transportDetails);
IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(ReceiveKeyActivity.this,
masterSecret);
identityKeyStore.saveIdentity(recipient.getRecipientId(), keyExchangeMessageBundle.getIdentityKey());
byte[] plaintext = cipher.decrypt(keyExchangeMessageBundle);
database.updateBundleMessageBody(masterSecret, messageId, "");
database.updateMessageBody(masterSecret, messageId, new String(plaintext));
} catch (InvalidKeyIdException | InvalidKeyException | LegacyMessageException | NoSessionException e) {
Log.w("ReceiveKeyActivity", e);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsCorruptKeyExchange(messageId);
} catch (InvalidMessageException e) {
Log.w("ReceiveKeyActivity", e);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsDecryptFailed(messageId);
} catch (DuplicateMessageException e) {
Log.w("ReceiveKeyActivity", e);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsDecryptDuplicate(messageId);
} catch (UntrustedIdentityException e) {
Log.w("ReceiveKeyActivity", e);
Toast.makeText(ReceiveKeyActivity.this, "Untrusted!", Toast.LENGTH_LONG).show();
}
} else if (identityUpdateMessage != null) {
DatabaseFactory.getIdentityDatabase(ReceiveKeyActivity.this)
.saveIdentity(masterSecret, recipient.getRecipientId(), identityUpdateMessage);
DatabaseFactory.getSmsDatabase(ReceiveKeyActivity.this).markAsProcessedKeyExchange(messageId);
if (message.isIdentityUpdate()) {
smsDatabase.markAsProcessedKeyExchange(messageId);
} else {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new SmsDecryptJob(context, messageId));
}
return null;
}
@@ -27,7 +27,7 @@ import com.google.i18n.phonenumbers.Phonenumber;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import org.whispersystems.textsecure.util.Util;
@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.service.RegistrationService;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.push.exceptions.ExpectationFailedException;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.exceptions.RateLimitException;
@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class RoutingActivity extends PassphraseRequiredActionBarActivity {
@@ -19,19 +19,16 @@ package org.thoughtcrime.securesms;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.WindowManager;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
/**
* An activity to quickly share content with contacts
@@ -31,7 +31,7 @@ import android.widget.ListView;
import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
/**
* A fragment to select and share to open conversations
@@ -27,8 +27,8 @@ import android.widget.AbsListView;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
/**
* A CursorAdapter for building a list of open conversations
@@ -29,10 +29,10 @@ import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.IdentityKeyParcelable;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
/**
* Activity for verifying identity keys.
@@ -20,7 +20,7 @@ import android.os.Bundle;
import android.widget.TextView;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.textsecure.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
/**
* Activity for displaying an identity key.
@@ -24,7 +24,7 @@ import android.view.MenuItem;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.whispersystems.textsecure.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
/**
* Activity that displays the local identity key and offers the option to regenerate it.
@@ -8,7 +8,7 @@ import android.view.View.OnClickListener;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RegistrationActivity;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class PushRegistrationReminder extends Reminder {
public static final long REMINDER_INTERVAL_MS = 3 * 24 * 60 * 60 * 1000;
@@ -9,7 +9,7 @@ import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.DatabaseMigrationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class SystemSmsImportReminder extends Reminder {
@@ -23,9 +23,6 @@ import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.PublicKey;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Util;
@@ -37,8 +37,6 @@ import javax.crypto.spec.SecretKeySpec;
import android.util.Log;
import org.whispersystems.textsecure.crypto.MasterSecret;
/**
* Class for streaming an encrypted MMS "part" off the disk.
*
@@ -1,481 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.PushReceiver;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.StaleKeyExchangeException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.TransportDetails;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushTransportDetails;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.SessionUtil;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
import ws.com.google.android.mms.pdu.PduParser;
import ws.com.google.android.mms.pdu.RetrieveConf;
/**
* A work queue for processing a number of encryption operations.
*
* @author Moxie Marlinspike
*/
public class DecryptingQueue {
private static final Executor executor = Executors.newSingleThreadExecutor();
public static void scheduleDecryption(Context context, MasterSecret masterSecret,
long messageId, long threadId, MultimediaMessagePdu mms)
{
MmsDecryptionItem runnable = new MmsDecryptionItem(context, masterSecret, messageId, threadId, mms);
executor.execute(runnable);
}
public static void scheduleDecryption(Context context, MasterSecret masterSecret,
long messageId, long threadId, String originator, int deviceId,
String body, boolean isSecureMessage, boolean isKeyExchange,
boolean isEndSession)
{
DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, threadId,
originator, deviceId, body,
isSecureMessage, isKeyExchange, isEndSession);
executor.execute(runnable);
}
public static void scheduleDecryption(Context context, MasterSecret masterSecret,
long messageId, IncomingPushMessage message)
{
PushDecryptionWorkItem runnable = new PushDecryptionWorkItem(context, masterSecret,
messageId, message);
executor.execute(runnable);
}
public static void schedulePendingDecrypts(Context context, MasterSecret masterSecret) {
Log.w("DecryptingQueue", "Processing pending decrypts...");
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(context);
EncryptingSmsDatabase.Reader smsReader = null;
PushDatabase.Reader pushReader = null;
SmsMessageRecord record;
IncomingPushMessage message;
try {
smsReader = smsDatabase.getDecryptInProgressMessages(masterSecret);
pushReader = pushDatabase.readerFor(pushDatabase.getPending());
while ((record = smsReader.getNext()) != null) {
scheduleDecryptFromCursor(context, masterSecret, record);
}
while ((message = pushReader.getNext()) != null) {
if (message.isPreKeyBundle()) {
Intent intent = new Intent(context, SendReceiveService.class);
intent.setAction(SendReceiveService.RECEIVE_PUSH_ACTION);
intent.putExtra("message", message);
context.startService(intent);
pushDatabase.delete(pushReader.getCurrentId());
} else {
scheduleDecryption(context, masterSecret, pushReader.getCurrentId(), message);
}
}
} finally {
if (smsReader != null)
smsReader.close();
if (pushReader != null)
pushReader.close();
}
}
public static void scheduleRogueMessages(Context context, MasterSecret masterSecret, Recipient recipient) {
SmsDatabase.Reader reader = null;
SmsMessageRecord record;
try {
Cursor cursor = DatabaseFactory.getSmsDatabase(context).getEncryptedRogueMessages(recipient);
reader = DatabaseFactory.getEncryptingSmsDatabase(context).readerFor(masterSecret, cursor);
while ((record = reader.getNext()) != null) {
DatabaseFactory.getSmsDatabase(context).markAsDecrypting(record.getId());
scheduleDecryptFromCursor(context, masterSecret, record);
}
} finally {
if (reader != null)
reader.close();
}
}
private static void scheduleDecryptFromCursor(Context context, MasterSecret masterSecret,
SmsMessageRecord record)
{
long messageId = record.getId();
long threadId = record.getThreadId();
String body = record.getBody().getBody();
String originator = record.getIndividualRecipient().getNumber();
int originatorDeviceId = record.getRecipientDeviceId();
boolean isSecureMessage = record.isSecure();
boolean isKeyExchange = record.isKeyExchange();
boolean isEndSession = record.isEndSession();
scheduleDecryption(context, masterSecret, messageId, threadId,
originator, originatorDeviceId, body,
isSecureMessage, isKeyExchange, isEndSession);
}
private static class PushDecryptionWorkItem implements Runnable {
private Context context;
private MasterSecret masterSecret;
private long messageId;
private IncomingPushMessage message;
public PushDecryptionWorkItem(Context context, MasterSecret masterSecret,
long messageId, IncomingPushMessage message)
{
this.context = context;
this.masterSecret = masterSecret;
this.messageId = messageId;
this.message = message;
}
public void run() {
try {
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false);
Recipient recipient = recipients.getPrimaryRecipient();
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSourceDevice());
if (!sessionStore.containsSession(recipientDevice.getRecipientId(), recipientDevice.getDeviceId())) {
sendResult(PushReceiver.RESULT_NO_SESSION);
return;
}
int sessionVersion = SessionUtil.getSessionVersion(context, masterSecret, recipientDevice);
TransportDetails transportDetails = new PushTransportDetails(sessionVersion);
TextSecureCipher textSecureCipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails);
byte[] plaintextBody = textSecureCipher.decrypt(new WhisperMessage(message.getBody()));
message = message.withBody(plaintextBody);
sendResult(PushReceiver.RESULT_OK);
} catch (InvalidMessageException | LegacyMessageException | RecipientFormattingException e) {
Log.w("DecryptionQueue", e);
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
} catch (DuplicateMessageException e) {
Log.w("DecryptingQueue", e);
sendResult(PushReceiver.RESULT_DECRYPT_DUPLICATE);
} catch (NoSessionException e) {
Log.w("DecryptingQueue", e);
sendResult(PushReceiver.RESULT_NO_SESSION);
}
}
private void sendResult(int result) {
Intent intent = new Intent(context, SendReceiveService.class);
intent.setAction(SendReceiveService.DECRYPTED_PUSH_ACTION);
intent.putExtra("message", message);
intent.putExtra("message_id", messageId);
intent.putExtra("result", result);
context.startService(intent);
}
}
private static class MmsDecryptionItem implements Runnable {
private long messageId;
private long threadId;
private Context context;
private MasterSecret masterSecret;
private MultimediaMessagePdu pdu;
public MmsDecryptionItem(Context context, MasterSecret masterSecret,
long messageId, long threadId, MultimediaMessagePdu pdu)
{
this.context = context;
this.masterSecret = masterSecret;
this.messageId = messageId;
this.threadId = threadId;
this.pdu = pdu;
}
private byte[] getEncryptedData() {
for (int i=0;i<pdu.getBody().getPartsNum();i++) {
Log.w("DecryptingQueue", "Content type (" + i + "): " + new String(pdu.getBody().getPart(i).getContentType()));
if (new String(pdu.getBody().getPart(i).getContentType()).equals(ContentType.TEXT_PLAIN)) {
return pdu.getBody().getPart(i).getData();
}
}
return null;
}
@Override
public void run() {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
try {
String messageFrom = pdu.getFrom().getString();
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false);
Recipient recipient = recipients.getPrimaryRecipient();
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
byte[] ciphertextPduBytes = getEncryptedData();
if (ciphertextPduBytes == null) {
Log.w("DecryptingQueue", "No encoded PNG data found on parts.");
database.markAsDecryptFailed(messageId, threadId);
return;
}
if (!sessionStore.containsSession(recipientDevice.getRecipientId(), recipientDevice.getDeviceId())) {
Log.w("DecryptingQueue", "No such recipient session for MMS...");
database.markAsNoSession(messageId, threadId);
return;
}
byte[] plaintextPduBytes;
Log.w("DecryptingQueue", "Decrypting: " + Hex.toString(ciphertextPduBytes));
TextTransport transportDetails = new TextTransport();
TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails);
byte[] decodedCiphertext = transportDetails.getDecodedMessage(ciphertextPduBytes);
try {
plaintextPduBytes = cipher.decrypt(new WhisperMessage(decodedCiphertext));
} catch (InvalidMessageException ime) {
// XXX - For some reason, Sprint seems to append a single character to the
// end of message text segments. I don't know why, so here we just try
// truncating the message by one if the MAC fails.
if (ciphertextPduBytes.length > 2) {
Log.w("DecryptingQueue", "Attempting truncated decrypt...");
byte[] truncated = Util.trim(ciphertextPduBytes, ciphertextPduBytes.length - 1);
decodedCiphertext = transportDetails.getDecodedMessage(truncated);
plaintextPduBytes = cipher.decrypt(new WhisperMessage(decodedCiphertext));
} else {
throw ime;
}
}
MultimediaMessagePdu plaintextGenericPdu = (MultimediaMessagePdu)new PduParser(plaintextPduBytes).parse();
RetrieveConf plaintextPdu = new RetrieveConf(plaintextGenericPdu.getPduHeaders(),
plaintextGenericPdu.getBody());
Log.w("DecryptingQueue", "Successfully decrypted MMS!");
database.insertSecureDecryptedMessageInbox(masterSecret, new IncomingMediaMessage(plaintextPdu), threadId);
database.delete(messageId);
} catch (RecipientFormattingException | IOException | MmsException | InvalidMessageException rfe) {
Log.w("DecryptingQueue", rfe);
database.markAsDecryptFailed(messageId, threadId);
} catch (DuplicateMessageException dme) {
Log.w("DecryptingQueue", dme);
database.markAsDecryptDuplicate(messageId, threadId);
} catch (LegacyMessageException lme) {
Log.w("DecryptingQueue", lme);
database.markAsLegacyVersion(messageId, threadId);
} catch (NoSessionException nse) {
Log.w("DecryptingQueue", nse);
database.markAsNoSession(messageId, threadId);
}
}
}
private static class DecryptionWorkItem implements Runnable {
private final long messageId;
private final long threadId;
private final Context context;
private final MasterSecret masterSecret;
private final String body;
private final String originator;
private final int deviceId;
private final boolean isSecureMessage;
private final boolean isKeyExchange;
private final boolean isEndSession;
public DecryptionWorkItem(Context context, MasterSecret masterSecret, long messageId, long threadId,
String originator, int deviceId, String body, boolean isSecureMessage,
boolean isKeyExchange, boolean isEndSession)
{
this.context = context;
this.messageId = messageId;
this.threadId = threadId;
this.masterSecret = masterSecret;
this.body = body;
this.originator = originator;
this.deviceId = deviceId;
this.isSecureMessage = isSecureMessage;
this.isKeyExchange = isKeyExchange;
this.isEndSession = isEndSession;
}
private void handleRemoteAsymmetricEncrypt() {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String plaintextBody;
try {
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false);
Recipient recipient = recipients.getPrimaryRecipient();
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), deviceId);
SmsTransportDetails transportDetails = new SmsTransportDetails();
byte[] decodedCiphertext = transportDetails.getDecodedMessage(body.getBytes());
if (!sessionStore.containsSession(recipientDevice.getRecipientId(), recipientDevice.getDeviceId())) {
if (WhisperMessage.isLegacy(decodedCiphertext)) database.markAsLegacyVersion(messageId);
else database.markAsNoSession(messageId);
return;
}
TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails);
byte[] paddedPlaintext = cipher.decrypt(new WhisperMessage(decodedCiphertext));
plaintextBody = new String(transportDetails.getStrippedPaddingMessageBody(paddedPlaintext));
if (isEndSession &&
"TERMINATE".equals(plaintextBody) &&
sessionStore.containsSession(recipientDevice.getRecipientId(), recipientDevice.getDeviceId()))
{
sessionStore.deleteSession(recipientDevice.getRecipientId(), recipientDevice.getDeviceId());
}
} catch (InvalidMessageException | IOException | RecipientFormattingException e) {
Log.w("DecryptionQueue", e);
database.markAsDecryptFailed(messageId);
return;
} catch (LegacyMessageException lme) {
Log.w("DecryptionQueue", lme);
database.markAsLegacyVersion(messageId);
return;
} catch (DuplicateMessageException e) {
Log.w("DecryptionQueue", e);
database.markAsDecryptDuplicate(messageId);
return;
} catch (NoSessionException e) {
Log.w("DecryptingQueue", e);
database.markAsNoSession(messageId);
return;
}
database.updateMessageBody(masterSecret, messageId, plaintextBody);
MessageNotifier.updateNotification(context, masterSecret);
}
private void handleLocalAsymmetricEncrypt() {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String plaintextBody;
try {
AsymmetricMasterCipher asymmetricMasterCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret));
plaintextBody = asymmetricMasterCipher.decryptBody(body);
if (isKeyExchange) {
handleKeyExchangeProcessing(plaintextBody);
}
database.updateMessageBody(masterSecret, messageId, plaintextBody);
MessageNotifier.updateNotification(context, masterSecret);
} catch (InvalidMessageException | IOException ime) {
Log.w("DecryptionQueue", ime);
database.markAsDecryptFailed(messageId);
}
}
private void handleKeyExchangeProcessing(String plaintextBody) {
if (TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) {
try {
Recipient recipient = RecipientFactory.getRecipientsFromString(context, originator, false)
.getPrimaryRecipient();
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), deviceId);
KeyExchangeMessage message = new KeyExchangeMessage(Base64.decodeWithoutPadding(plaintextBody));
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipientDevice);
processor.processKeyExchangeMessage(message, threadId);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsProcessedKeyExchange(messageId);
} catch (InvalidVersionException e) {
Log.w("DecryptingQueue", e);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsInvalidVersionKeyExchange(messageId);
} catch (InvalidMessageException | IOException | InvalidKeyException | RecipientFormattingException e) {
Log.w("DecryptingQueue", e);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId);
} catch (LegacyMessageException e) {
Log.w("DecryptingQueue", e);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsLegacyVersion(messageId);
} catch (StaleKeyExchangeException e) {
Log.w("DecryptingQueue", e);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId);
} catch (UntrustedIdentityException e) {
Log.w("DecryptingQueue", e);
}
}
}
@Override
public void run() {
if (isSecureMessage || isEndSession) {
handleRemoteAsymmetricEncrypt();
} else {
handleLocalAsymmetricEncrypt();
}
}
}
}
@@ -32,8 +32,6 @@ import javax.crypto.spec.SecretKeySpec;
import android.util.Log;
import org.whispersystems.textsecure.crypto.MasterSecret;
/**
* A class for streaming an encrypted MMS "part" to disk.
*
@@ -0,0 +1,69 @@
/**
* Copyright (C) 2014 Open 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import android.os.Parcel;
import android.os.Parcelable;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
public class IdentityKeyParcelable implements Parcelable {
public static final Parcelable.Creator<IdentityKeyParcelable> CREATOR = new Parcelable.Creator<IdentityKeyParcelable>() {
public IdentityKeyParcelable createFromParcel(Parcel in) {
try {
return new IdentityKeyParcelable(in);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public IdentityKeyParcelable[] newArray(int size) {
return new IdentityKeyParcelable[size];
}
};
private final IdentityKey identityKey;
public IdentityKeyParcelable(IdentityKey identityKey) {
this.identityKey = identityKey;
}
public IdentityKeyParcelable(Parcel in) throws InvalidKeyException {
int serializedLength = in.readInt();
byte[] serialized = new byte[serializedLength];
in.readByteArray(serialized);
this.identityKey = new IdentityKey(serialized, 0);
}
public IdentityKey get() {
return identityKey;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(identityKey.serialize().length);
dest.writeByteArray(identityKey.serialize());
}
}
@@ -28,8 +28,6 @@ import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.Base64;
import java.io.IOException;
@@ -22,6 +22,9 @@ import android.content.Context;
import android.content.DialogInterface;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
@@ -33,10 +36,7 @@ import org.whispersystems.libaxolotl.state.IdentityKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.TextSecurePreKeyStore;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
import org.whispersystems.textsecure.util.Base64;
public class KeyExchangeInitiator {
@@ -1,98 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.Intent;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.PreKeyService;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.SessionBuilder;
import org.whispersystems.libaxolotl.StaleKeyExchangeException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.libaxolotl.state.IdentityKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.TextSecurePreKeyStore;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
import org.whispersystems.textsecure.util.Base64;
/**
* This class processes key exchange interactions.
*
* @author Moxie Marlinspike
*/
public class KeyExchangeProcessor {
public static final String SECURITY_UPDATE_EVENT = "org.thoughtcrime.securesms.KEY_EXCHANGE_UPDATE";
private Context context;
private RecipientDevice recipientDevice;
private MasterSecret masterSecret;
private SessionBuilder sessionBuilder;
public KeyExchangeProcessor(Context context, MasterSecret masterSecret, RecipientDevice recipientDevice)
{
this.context = context;
this.recipientDevice = recipientDevice;
this.masterSecret = masterSecret;
IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context, masterSecret);
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret);
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
this.sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore,
identityKeyStore, recipientDevice.getRecipientId(),
recipientDevice.getDeviceId());
}
public void processKeyExchangeMessage(PreKeyBundle bundle, long threadId)
throws InvalidKeyException, UntrustedIdentityException
{
sessionBuilder.process(bundle);
if (threadId != -1) {
broadcastSecurityUpdateEvent(context, threadId);
}
}
public OutgoingKeyExchangeMessage processKeyExchangeMessage(KeyExchangeMessage message, long threadId)
throws InvalidKeyException, UntrustedIdentityException, StaleKeyExchangeException
{
KeyExchangeMessage responseMessage = sessionBuilder.process(message);
Recipient recipient = RecipientFactory.getRecipientsForIds(context,
String.valueOf(recipientDevice.getRecipientId()),
false)
.getPrimaryRecipient();
DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient);
broadcastSecurityUpdateEvent(context, threadId);
if (responseMessage != null) {
String serializedResponse = Base64.encodeBytesWithoutPadding(responseMessage.serialize());
return new OutgoingKeyExchangeMessage(recipient, serializedResponse);
} else {
return null;
}
}
public static void broadcastSecurityUpdateEvent(Context context, long threadId) {
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", threadId);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
}
}
@@ -0,0 +1,220 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import android.util.Log;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Hex;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Class that handles encryption for local storage.
*
* The protocol format is roughly:
*
* 1) 16 byte random IV.
* 2) AES-CBC(plaintext)
* 3) HMAC-SHA1 of 1 and 2
*
* @author Moxie Marlinspike
*/
public class MasterCipher {
private final MasterSecret masterSecret;
private final Cipher encryptingCipher;
private final Cipher decryptingCipher;
private final Mac hmac;
public MasterCipher(MasterSecret masterSecret) {
try {
this.masterSecret = masterSecret;
this.encryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.decryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.hmac = Mac.getInstance("HmacSHA1");
} catch (NoSuchPaddingException nspe) {
throw new AssertionError(nspe);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public byte[] encryptKey(ECPrivateKey privateKey) {
return encryptBytes(privateKey.serialize());
}
public String encryptBody(String body) {
return encryptAndEncodeBytes(body.getBytes());
}
public String decryptBody(String body) throws InvalidMessageException {
return new String(decodeAndDecryptBytes(body));
}
public ECPrivateKey decryptKey(byte[] key)
throws org.whispersystems.libaxolotl.InvalidKeyException
{
try {
return Curve.decodePrivatePoint(decryptBytes(key));
} catch (InvalidMessageException ime) {
throw new org.whispersystems.libaxolotl.InvalidKeyException(ime);
}
}
public byte[] decryptBytes(byte[] decodedBody) throws InvalidMessageException {
try {
Mac mac = getMac(masterSecret.getMacKey());
byte[] encryptedBody = verifyMacBody(mac, decodedBody);
Cipher cipher = getDecryptingCipher(masterSecret.getEncryptionKey(), encryptedBody);
byte[] encrypted = getDecryptedBody(cipher, encryptedBody);
return encrypted;
} catch (GeneralSecurityException ge) {
throw new InvalidMessageException(ge);
}
}
public byte[] encryptBytes(byte[] body) {
try {
Cipher cipher = getEncryptingCipher(masterSecret.getEncryptionKey());
Mac mac = getMac(masterSecret.getMacKey());
byte[] encryptedBody = getEncryptedBody(cipher, body);
byte[] encryptedAndMacBody = getMacBody(mac, encryptedBody);
return encryptedAndMacBody;
} catch (GeneralSecurityException ge) {
Log.w("bodycipher", ge);
return null;
}
}
public boolean verifyMacFor(String content, byte[] theirMac) {
byte[] ourMac = getMacFor(content);
Log.w("MasterCipher", "Our Mac: " + Hex.toString(ourMac));
Log.w("MasterCipher", "Thr Mac: " + Hex.toString(theirMac));
return Arrays.equals(ourMac, theirMac);
}
public byte[] getMacFor(String content) {
Log.w("MasterCipher", "Macing: " + content);
try {
Mac mac = getMac(masterSecret.getMacKey());
return mac.doFinal(content.getBytes());
} catch (GeneralSecurityException ike) {
throw new AssertionError(ike);
}
}
private byte[] decodeAndDecryptBytes(String body) throws InvalidMessageException {
try {
byte[] decodedBody = Base64.decode(body);
return decryptBytes(decodedBody);
} catch (IOException e) {
throw new InvalidMessageException("Bad Base64 Encoding...", e);
}
}
private String encryptAndEncodeBytes(byte[] bytes) {
byte[] encryptedAndMacBody = encryptBytes(bytes);
return Base64.encodeBytes(encryptedAndMacBody);
}
private byte[] verifyMacBody(Mac hmac, byte[] encryptedAndMac) throws InvalidMessageException {
byte[] encrypted = new byte[encryptedAndMac.length - hmac.getMacLength()];
System.arraycopy(encryptedAndMac, 0, encrypted, 0, encrypted.length);
byte[] remoteMac = new byte[hmac.getMacLength()];
System.arraycopy(encryptedAndMac, encryptedAndMac.length - remoteMac.length, remoteMac, 0, remoteMac.length);
byte[] localMac = hmac.doFinal(encrypted);
if (!Arrays.equals(remoteMac, localMac))
throw new InvalidMessageException("MAC doesen't match.");
return encrypted;
}
private byte[] getDecryptedBody(Cipher cipher, byte[] encryptedBody) throws IllegalBlockSizeException, BadPaddingException {
return cipher.doFinal(encryptedBody, cipher.getBlockSize(), encryptedBody.length - cipher.getBlockSize());
}
private byte[] getEncryptedBody(Cipher cipher, byte[] body) throws IllegalBlockSizeException, BadPaddingException {
byte[] encrypted = cipher.doFinal(body);
byte[] iv = cipher.getIV();
byte[] ivAndBody = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, ivAndBody, 0, iv.length);
System.arraycopy(encrypted, 0, ivAndBody, iv.length, encrypted.length);
return ivAndBody;
}
private Mac getMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
// Mac hmac = Mac.getInstance("HmacSHA1");
hmac.init(key);
return hmac;
}
private byte[] getMacBody(Mac hmac, byte[] encryptedBody) {
byte[] mac = hmac.doFinal(encryptedBody);
byte[] encryptedAndMac = new byte[encryptedBody.length + mac.length];
System.arraycopy(encryptedBody, 0, encryptedAndMac, 0, encryptedBody.length);
System.arraycopy(mac, 0, encryptedAndMac, encryptedBody.length, mac.length);
return encryptedAndMac;
}
private Cipher getDecryptingCipher(SecretKeySpec key, byte[] encryptedBody) throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(encryptedBody, 0, decryptingCipher.getBlockSize());
decryptingCipher.init(Cipher.DECRYPT_MODE, key, iv);
return decryptingCipher;
}
private Cipher getEncryptingCipher(SecretKeySpec key) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
encryptingCipher.init(Cipher.ENCRYPT_MODE, key);
return encryptingCipher;
}
}
@@ -0,0 +1,119 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import android.os.Parcel;
import android.os.Parcelable;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;
/**
* When a user first initializes TextSecure, a few secrets
* are generated. These are:
*
* 1) A 128bit symmetric encryption key.
* 2) A 160bit symmetric MAC key.
* 3) An ECC keypair.
*
* The first two, along with the ECC keypair's private key, are
* then encrypted on disk using PBE.
*
* This class represents 1 and 2.
*
* @author Moxie Marlinspike
*/
public class MasterSecret implements Parcelable {
private final SecretKeySpec encryptionKey;
private final SecretKeySpec macKey;
public static final Parcelable.Creator<MasterSecret> CREATOR = new Parcelable.Creator<MasterSecret>() {
@Override
public MasterSecret createFromParcel(Parcel in) {
return new MasterSecret(in);
}
@Override
public MasterSecret[] newArray(int size) {
return new MasterSecret[size];
}
};
public MasterSecret(SecretKeySpec encryptionKey, SecretKeySpec macKey) {
this.encryptionKey = encryptionKey;
this.macKey = macKey;
}
private MasterSecret(Parcel in) {
byte[] encryptionKeyBytes = new byte[in.readInt()];
in.readByteArray(encryptionKeyBytes);
byte[] macKeyBytes = new byte[in.readInt()];
in.readByteArray(macKeyBytes);
this.encryptionKey = new SecretKeySpec(encryptionKeyBytes, "AES");
this.macKey = new SecretKeySpec(macKeyBytes, "HmacSHA1");
// SecretKeySpec does an internal copy in its constructor.
Arrays.fill(encryptionKeyBytes, (byte) 0x00);
Arrays.fill(macKeyBytes, (byte)0x00);
}
public SecretKeySpec getEncryptionKey() {
return this.encryptionKey;
}
public SecretKeySpec getMacKey() {
return this.macKey;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(encryptionKey.getEncoded().length);
out.writeByteArray(encryptionKey.getEncoded());
out.writeInt(macKey.getEncoded().length);
out.writeByteArray(macKey.getEncoded());
}
@Override
public int describeContents() {
return 0;
}
public MasterSecret parcelClone() {
Parcel thisParcel = Parcel.obtain();
Parcel thatParcel = Parcel.obtain();
byte[] bytes = null;
thisParcel.writeValue(this);
bytes = thisParcel.marshall();
thatParcel.unmarshall(bytes, 0, bytes.length);
thatParcel.setDataPosition(0);
MasterSecret that = (MasterSecret)thatParcel.readValue(MasterSecret.class.getClassLoader());
thisParcel.recycle();
thatParcel.recycle();
return that;
}
}
@@ -26,8 +26,6 @@ import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Util;
@@ -0,0 +1,134 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.crypto.TextSecureCipher;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduComposer;
import ws.com.google.android.mms.pdu.PduParser;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.RetrieveConf;
import ws.com.google.android.mms.pdu.SendReq;
public class MmsCipher {
private static final String TAG = MmsCipher.class.getSimpleName();
private final TextTransport textTransport = new TextTransport();
private final AxolotlStore axolotlStore;
public MmsCipher(AxolotlStore axolotlStore) {
this.axolotlStore = axolotlStore;
}
public MultimediaMessagePdu decrypt(Context context, MultimediaMessagePdu pdu)
throws InvalidMessageException, LegacyMessageException, DuplicateMessageException,
NoSessionException
{
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, pdu.getFrom().getString(), false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
SessionCipher sessionCipher = new SessionCipher(axolotlStore, recipientId, 1);
Optional<byte[]> ciphertext = getEncryptedData(pdu);
if (!ciphertext.isPresent()) {
throw new InvalidMessageException("No ciphertext present!");
}
byte[] decodedCiphertext = textTransport.getDecodedMessage(ciphertext.get());
byte[] plaintext;
try {
plaintext = sessionCipher.decrypt(new WhisperMessage(decodedCiphertext));
} catch (InvalidMessageException e) {
// NOTE - For some reason, Sprint seems to append a single character to the
// end of message text segments. I don't know why, so here we just try
// truncating the message by one if the MAC fails.
if (ciphertext.get().length > 2) {
Log.w(TAG, "Attempting truncated decrypt...");
byte[] truncated = Util.trim(ciphertext.get(), ciphertext.get().length - 1);
decodedCiphertext = textTransport.getDecodedMessage(truncated);
plaintext = sessionCipher.decrypt(new WhisperMessage(decodedCiphertext));
} else {
throw e;
}
}
MultimediaMessagePdu plaintextGenericPdu = (MultimediaMessagePdu) new PduParser(plaintext).parse();
return new RetrieveConf(plaintextGenericPdu.getPduHeaders(), plaintextGenericPdu.getBody());
} catch (RecipientFormattingException | IOException e) {
throw new InvalidMessageException(e);
}
}
public SendReq encrypt(Context context, SendReq message)
throws NoSessionException, RecipientFormattingException
{
EncodedStringValue[] encodedRecipient = message.getTo();
String recipientString = encodedRecipient[0].getString();
Recipients recipients = RecipientFactory.getRecipientsFromString(context, recipientString, false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
byte[] pduBytes = new PduComposer(context, message).make();
if (!axolotlStore.containsSession(recipientId, RecipientDevice.DEFAULT_DEVICE_ID)) {
throw new NoSessionException("No session for: " + recipientId);
}
SessionCipher cipher = new SessionCipher(axolotlStore, recipientId, RecipientDevice.DEFAULT_DEVICE_ID);
CiphertextMessage ciphertextMessage = cipher.encrypt(textTransport.getPaddedMessageBody(pduBytes));
byte[] encryptedPduBytes = textTransport.getEncodedMessage(ciphertextMessage.serialize());
PduBody body = new PduBody();
PduPart part = new PduPart();
SendReq encryptedPdu = new SendReq(message.getPduHeaders(), body);
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setContentType(ContentType.TEXT_PLAIN.getBytes());
part.setName((System.currentTimeMillis()+"").getBytes());
part.setData(encryptedPduBytes);
body.addPart(part);
encryptedPdu.setSubject(new EncodedStringValue(WirePrefix.calculateEncryptedMmsSubject()));
encryptedPdu.setBody(body);
return encryptedPdu;
}
private Optional<byte[]> getEncryptedData(MultimediaMessagePdu pdu) {
for (int i=0;i<pdu.getBody().getPartsNum();i++) {
if (new String(pdu.getBody().getPart(i).getContentType()).equals(ContentType.TEXT_PLAIN)) {
return Optional.of(pdu.getBody().getPart(i).getData());
}
}
return Optional.absent();
}
}
@@ -0,0 +1,208 @@
/**
* Copyright (C) 2013 Open 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.Curve25519;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.whispersystems.libaxolotl.util.Medium;
import org.whispersystems.textsecure.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.List;
public class PreKeyUtil {
public static final int BATCH_SIZE = 100;
public static List<PreKeyRecord> generatePreKeys(Context context, MasterSecret masterSecret) {
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
List<PreKeyRecord> records = new LinkedList<>();
int preKeyIdOffset = getNextPreKeyId(context);
for (int i=0;i<BATCH_SIZE;i++) {
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
ECKeyPair keyPair = Curve25519.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
preKeyStore.storePreKey(preKeyId, record);
records.add(record);
}
setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
return records;
}
public static SignedPreKeyRecord generateSignedPreKey(Context context, MasterSecret masterSecret,
IdentityKeyPair identityKeyPair)
{
try {
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret);
int signedPreKeyId = getNextSignedPreKeyId(context);
ECKeyPair keyPair = Curve25519.generateKeyPair();
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE);
return record;
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static PreKeyRecord generateLastResortKey(Context context, MasterSecret masterSecret) {
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
if (preKeyStore.containsPreKey(Medium.MAX_VALUE)) {
try {
return preKeyStore.loadPreKey(Medium.MAX_VALUE);
} catch (InvalidKeyIdException e) {
Log.w("PreKeyUtil", e);
preKeyStore.removePreKey(Medium.MAX_VALUE);
}
}
ECKeyPair keyPair = Curve25519.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
preKeyStore.storePreKey(Medium.MAX_VALUE, record);
return record;
}
private static void setNextPreKeyId(Context context, int id) {
try {
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
FileOutputStream fout = new FileOutputStream(nextFile);
fout.write(new Gson().toJson(new PreKeyIndex(id)).getBytes());
fout.close();
} catch (IOException e) {
Log.w("PreKeyUtil", e);
}
}
private static void setNextSignedPreKeyId(Context context, int id) {
try {
File nextFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME);
FileOutputStream fout = new FileOutputStream(nextFile);
fout.write(new Gson().toJson(new SignedPreKeyIndex(id)).getBytes());
fout.close();
} catch (IOException e) {
Log.w("PreKeyUtil", e);
}
}
private static int getNextPreKeyId(Context context) {
try {
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
if (!nextFile.exists()) {
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
} else {
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
PreKeyIndex index = new Gson().fromJson(reader, PreKeyIndex.class);
reader.close();
return index.nextPreKeyId;
}
} catch (IOException e) {
Log.w("PreKeyUtil", e);
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
}
}
private static int getNextSignedPreKeyId(Context context) {
try {
File nextFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME);
if (!nextFile.exists()) {
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
} else {
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
SignedPreKeyIndex index = new Gson().fromJson(reader, SignedPreKeyIndex.class);
reader.close();
return index.nextSignedPreKeyId;
}
} catch (IOException e) {
Log.w("PreKeyUtil", e);
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
}
}
private static File getPreKeysDirectory(Context context) {
return getKeysDirectory(context, TextSecurePreKeyStore.PREKEY_DIRECTORY);
}
private static File getSignedPreKeysDirectory(Context context) {
return getKeysDirectory(context, TextSecurePreKeyStore.SIGNED_PREKEY_DIRECTORY);
}
private static File getKeysDirectory(Context context, String name) {
File directory = new File(context.getFilesDir(), name);
if (!directory.exists())
directory.mkdirs();
return directory;
}
private static class PreKeyIndex {
public static final String FILE_NAME = "index.dat";
private int nextPreKeyId;
public PreKeyIndex() {}
public PreKeyIndex(int nextPreKeyId) {
this.nextPreKeyId = nextPreKeyId;
}
}
private static class SignedPreKeyIndex {
public static final String FILE_NAME = "index.dat";
private int nextSignedPreKeyId;
public SignedPreKeyIndex() {}
public SignedPreKeyIndex(int nextSignedPreKeyId) {
this.nextSignedPreKeyId = nextSignedPreKeyId;
}
}
}
@@ -0,0 +1,103 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import android.util.Log;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class PublicKey {
public static final int KEY_SIZE = 3 + ECPublicKey.KEY_SIZE;
private final ECPublicKey publicKey;
private int id;
public PublicKey(PublicKey publicKey) {
this.id = publicKey.id;
// FIXME :: This not strictly an accurate copy constructor.
this.publicKey = publicKey.publicKey;
}
public PublicKey(int id, ECPublicKey publicKey) {
this.publicKey = publicKey;
this.id = id;
}
public PublicKey(byte[] bytes, int offset) throws InvalidKeyException {
Log.w("PublicKey", "PublicKey Length: " + (bytes.length - offset));
if ((bytes.length - offset) < KEY_SIZE)
throw new InvalidKeyException("Provided bytes are too short.");
this.id = Conversions.byteArrayToMedium(bytes, offset);
this.publicKey = Curve.decodePoint(bytes, offset + 3);
}
public PublicKey(byte[] bytes) throws InvalidKeyException {
this(bytes, 0);
}
public int getType() {
return publicKey.getType();
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public ECPublicKey getKey() {
return publicKey;
}
public String getFingerprint() {
return Hex.toString(getFingerprintBytes());
}
public byte[] getFingerprintBytes() {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return md.digest(serialize());
} catch (NoSuchAlgorithmException nsae) {
Log.w("LocalKeyPair", nsae);
throw new IllegalArgumentException("SHA-1 isn't supported!");
}
}
public byte[] serialize() {
byte[] keyIdBytes = Conversions.mediumToByteArray(id);
byte[] serializedPoint = publicKey.serialize();
Log.w("PublicKey", "Serializing public key point: " + Hex.toString(serializedPoint));
return Util.combine(keyIdBytes, serializedPoint);
}
}
@@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.Intent;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.SessionBuilder;
import org.whispersystems.libaxolotl.StaleKeyExchangeException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.libaxolotl.state.IdentityKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.Base64;
/**
* This class processes key exchange interactions.
*
* @author Moxie Marlinspike
*/
public class SecurityEvent {
public static final String SECURITY_UPDATE_EVENT = "org.thoughtcrime.securesms.KEY_EXCHANGE_UPDATE";
public static void broadcastSecurityUpdateEvent(Context context, long threadId) {
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", threadId);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
}
}
@@ -0,0 +1,129 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.OutgoingPrekeyBundleMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionBuilder;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.StaleKeyExchangeException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.textsecure.storage.RecipientDevice;
import java.io.IOException;
public class SmsCipher {
private final SmsTransportDetails transportDetails = new SmsTransportDetails();
private final AxolotlStore axolotlStore;
public SmsCipher(AxolotlStore axolotlStore) {
this.axolotlStore = axolotlStore;
}
public IncomingTextMessage decrypt(Context context, IncomingTextMessage message)
throws LegacyMessageException, InvalidMessageException,
DuplicateMessageException, NoSessionException
{
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSender(), false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
byte[] decoded = transportDetails.getDecodedMessage(message.getMessageBody().getBytes());
WhisperMessage whisperMessage = new WhisperMessage(decoded);
SessionCipher sessionCipher = new SessionCipher(axolotlStore, recipientId, 1);
byte[] padded = sessionCipher.decrypt(whisperMessage);
byte[] plaintext = transportDetails.getStrippedPaddingMessageBody(padded);
if (message.isEndSession() && "TERMINATE".equals(new String(plaintext))) {
axolotlStore.deleteSession(recipientId, 1);
}
return message.withMessageBody(new String(plaintext));
} catch (RecipientFormattingException | IOException e) {
throw new InvalidMessageException(e);
}
}
public IncomingEncryptedMessage decrypt(Context context, IncomingPreKeyBundleMessage message)
throws InvalidVersionException, InvalidMessageException, DuplicateMessageException,
UntrustedIdentityException, LegacyMessageException
{
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSender(), false);
byte[] decoded = transportDetails.getDecodedMessage(message.getMessageBody().getBytes());
PreKeyWhisperMessage preKeyMessage = new PreKeyWhisperMessage(decoded);
SessionCipher sessionCipher = new SessionCipher(axolotlStore, recipients.getPrimaryRecipient().getRecipientId(), 1);
byte[] padded = sessionCipher.decrypt(preKeyMessage);
byte[] plaintext = transportDetails.getStrippedPaddingMessageBody(padded);
return new IncomingEncryptedMessage(message, new String(plaintext));
} catch (RecipientFormattingException | IOException | InvalidKeyException | InvalidKeyIdException e) {
throw new InvalidMessageException(e);
}
}
public OutgoingTextMessage encrypt(OutgoingTextMessage message) throws NoSessionException {
byte[] paddedBody = transportDetails.getPaddedMessageBody(message.getMessageBody().getBytes());
long recipientId = message.getRecipients().getPrimaryRecipient().getRecipientId();
if (!axolotlStore.containsSession(recipientId, RecipientDevice.DEFAULT_DEVICE_ID)) {
throw new NoSessionException("No session for: " + recipientId);
}
SessionCipher cipher = new SessionCipher(axolotlStore, recipientId, RecipientDevice.DEFAULT_DEVICE_ID);
CiphertextMessage ciphertextMessage = cipher.encrypt(paddedBody);
String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize()));
if (ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE) {
return new OutgoingPrekeyBundleMessage(message, encodedCiphertext);
} else {
return message.withBody(encodedCiphertext);
}
}
public OutgoingKeyExchangeMessage process(Context context, IncomingKeyExchangeMessage message)
throws UntrustedIdentityException, StaleKeyExchangeException,
InvalidVersionException, LegacyMessageException, InvalidMessageException
{
try {
Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSender(), false).getPrimaryRecipient();
KeyExchangeMessage exchangeMessage = new KeyExchangeMessage(transportDetails.getDecodedMessage(message.getMessageBody().getBytes()));
SessionBuilder sessionBuilder = new SessionBuilder(axolotlStore, recipient.getRecipientId(), 1);
KeyExchangeMessage response = sessionBuilder.process(exchangeMessage);
if (response != null) {
byte[] serializedResponse = transportDetails.getEncodedMessage(response.serialize());
return new OutgoingKeyExchangeMessage(recipient, new String(serializedResponse));
} else {
return null;
}
} catch (RecipientFormattingException | IOException | InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
}
@@ -1,67 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.IdentityKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.TransportDetails;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.TextSecurePreKeyStore;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
public class TextSecureCipher {
private final SessionCipher sessionCipher;
private final TransportDetails transportDetails;
public TextSecureCipher(Context context, MasterSecret masterSecret,
RecipientDevice recipient, TransportDetails transportDetails)
{
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret);
IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context, masterSecret);
this.transportDetails = transportDetails;
this.sessionCipher = new SessionCipher(sessionStore, preKeyStore, signedPreKeyStore, identityKeyStore,
recipient.getRecipientId(), recipient.getDeviceId());
}
public CiphertextMessage encrypt(byte[] unpaddedMessage) {
return sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
}
public byte[] decrypt(WhisperMessage message)
throws DuplicateMessageException, LegacyMessageException, InvalidMessageException, NoSessionException
{
byte[] paddedMessage = sessionCipher.decrypt(message);
return transportDetails.getStrippedPaddingMessageBody(paddedMessage);
}
public byte[] decrypt(PreKeyWhisperMessage message)
throws InvalidKeyException, LegacyMessageException, InvalidMessageException,
DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException, NoSessionException
{
byte[] paddedMessage = sessionCipher.decrypt(message);
return transportDetails.getStrippedPaddingMessageBody(paddedMessage);
}
public int getRemoteRegistrationId() {
return sessionCipher.getRemoteRegistrationId();
}
}
@@ -0,0 +1,128 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.state.IdentityKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import java.util.List;
public class TextSecureAxolotlStore implements AxolotlStore {
private final PreKeyStore preKeyStore;
private final SignedPreKeyStore signedPreKeyStore;
private final IdentityKeyStore identityKeyStore;
private final SessionStore sessionStore;
public TextSecureAxolotlStore(Context context, MasterSecret masterSecret) {
this.preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
this.signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret);
this.identityKeyStore = new TextSecureIdentityKeyStore(context, masterSecret);
this.sessionStore = new TextSecureSessionStore(context, masterSecret);
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyStore.getIdentityKeyPair();
}
@Override
public int getLocalRegistrationId() {
return identityKeyStore.getLocalRegistrationId();
}
@Override
public void saveIdentity(long recipientId, IdentityKey identityKey) {
identityKeyStore.saveIdentity(recipientId, identityKey);
}
@Override
public boolean isTrustedIdentity(long recipientId, IdentityKey identityKey) {
return identityKeyStore.isTrustedIdentity(recipientId, identityKey);
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
return preKeyStore.loadPreKey(preKeyId);
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
preKeyStore.storePreKey(preKeyId, record);
}
@Override
public boolean containsPreKey(int preKeyId) {
return preKeyStore.containsPreKey(preKeyId);
}
@Override
public void removePreKey(int preKeyId) {
preKeyStore.removePreKey(preKeyId);
}
@Override
public SessionRecord loadSession(long recipientId, int deviceId) {
return sessionStore.loadSession(recipientId, deviceId);
}
@Override
public List<Integer> getSubDeviceSessions(long recipientId) {
return sessionStore.getSubDeviceSessions(recipientId);
}
@Override
public void storeSession(long recipientId, int deviceId, SessionRecord record) {
sessionStore.storeSession(recipientId, deviceId, record);
}
@Override
public boolean containsSession(long recipientId, int deviceId) {
return sessionStore.containsSession(recipientId, deviceId);
}
@Override
public void deleteSession(long recipientId, int deviceId) {
sessionStore.deleteSession(recipientId, deviceId);
}
@Override
public void deleteAllSessions(long recipientId) {
sessionStore.deleteAllSessions(recipientId);
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
return signedPreKeyStore.loadSignedPreKey(signedPreKeyId);
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
return signedPreKeyStore.loadSignedPreKeys();
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return signedPreKeyStore.containsSignedPreKey(signedPreKeyId);
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
signedPreKeyStore.removeSignedPreKey(signedPreKeyId);
}
}
@@ -1,13 +1,14 @@
package org.thoughtcrime.securesms.crypto;
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.state.IdentityKeyStore;
import org.whispersystems.textsecure.crypto.MasterSecret;
public class TextSecureIdentityKeyStore implements IdentityKeyStore {
@@ -0,0 +1,212 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.whispersystems.textsecure.util.Conversions;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.LinkedList;
import java.util.List;
public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
public static final String PREKEY_DIRECTORY = "prekeys";
public static final String SIGNED_PREKEY_DIRECTORY = "signed_prekeys";
private static final int CURRENT_VERSION_MARKER = 1;
private static final Object FILE_LOCK = new Object();
private static final String TAG = TextSecurePreKeyStore.class.getSimpleName();
private final Context context;
private final MasterSecret masterSecret;
public TextSecurePreKeyStore(Context context, MasterSecret masterSecret) {
this.context = context;
this.masterSecret = masterSecret;
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
synchronized (FILE_LOCK) {
try {
return new PreKeyRecord(loadSerializedRecord(getPreKeyFile(preKeyId)));
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, e);
throw new InvalidKeyIdException(e);
}
}
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
synchronized (FILE_LOCK) {
try {
return new SignedPreKeyRecord(loadSerializedRecord(getSignedPreKeyFile(signedPreKeyId)));
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, e);
throw new InvalidKeyIdException(e);
}
}
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
synchronized (FILE_LOCK) {
File directory = getSignedPreKeyDirectory();
List<SignedPreKeyRecord> results = new LinkedList<>();
for (File signedPreKeyFile : directory.listFiles()) {
try {
results.add(new SignedPreKeyRecord(loadSerializedRecord(signedPreKeyFile)));
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, e);
}
}
return results;
}
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
synchronized (FILE_LOCK) {
try {
storeSerializedRecord(getPreKeyFile(preKeyId), record.serialize());
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
synchronized (FILE_LOCK) {
try {
storeSerializedRecord(getSignedPreKeyFile(signedPreKeyId), record.serialize());
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
@Override
public boolean containsPreKey(int preKeyId) {
File record = getPreKeyFile(preKeyId);
return record.exists();
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
File record = getSignedPreKeyFile(signedPreKeyId);
return record.exists();
}
@Override
public void removePreKey(int preKeyId) {
File record = getPreKeyFile(preKeyId);
record.delete();
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
File record = getSignedPreKeyFile(signedPreKeyId);
record.delete();
}
private byte[] loadSerializedRecord(File recordFile)
throws IOException, InvalidMessageException
{
MasterCipher masterCipher = new MasterCipher(masterSecret);
FileInputStream fin = new FileInputStream(recordFile);
int recordVersion = readInteger(fin);
if (recordVersion != CURRENT_VERSION_MARKER) {
throw new AssertionError("Invalid version: " + recordVersion);
}
return masterCipher.decryptBytes(readBlob(fin));
}
private void storeSerializedRecord(File file, byte[] serialized) throws IOException {
MasterCipher masterCipher = new MasterCipher(masterSecret);
RandomAccessFile recordFile = new RandomAccessFile(file, "rw");
FileChannel out = recordFile.getChannel();
out.position(0);
writeInteger(CURRENT_VERSION_MARKER, out);
writeBlob(masterCipher.encryptBytes(serialized), out);
out.truncate(out.position());
recordFile.close();
}
private File getPreKeyFile(int preKeyId) {
return new File(getPreKeyDirectory(), String.valueOf(preKeyId));
}
private File getSignedPreKeyFile(int signedPreKeyId) {
return new File(getSignedPreKeyDirectory(), String.valueOf(signedPreKeyId));
}
private File getPreKeyDirectory() {
return getRecordsDirectory(PREKEY_DIRECTORY);
}
private File getSignedPreKeyDirectory() {
return getRecordsDirectory(SIGNED_PREKEY_DIRECTORY);
}
private File getRecordsDirectory(String directoryName) {
File directory = new File(context.getFilesDir(), directoryName);
if (!directory.exists()) {
if (!directory.mkdirs()) {
Log.w(TAG, "PreKey directory creation failed!");
}
}
return directory;
}
private byte[] readBlob(FileInputStream in) throws IOException {
int length = readInteger(in);
byte[] blobBytes = new byte[length];
in.read(blobBytes, 0, blobBytes.length);
return blobBytes;
}
private void writeBlob(byte[] blobBytes, FileChannel out) throws IOException {
writeInteger(blobBytes.length, out);
out.write(ByteBuffer.wrap(blobBytes));
}
private int readInteger(FileInputStream in) throws IOException {
byte[] integer = new byte[4];
in.read(integer, 0, integer.length);
return Conversions.byteArrayToInt(integer);
}
private void writeInteger(int value, FileChannel out) throws IOException {
byte[] valueBytes = Conversions.intToByteArray(value);
out.write(ByteBuffer.wrap(valueBytes));
}
}
@@ -0,0 +1,186 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SessionState;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.Conversions;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.libaxolotl.state.StorageProtos.SessionStructure;
public class TextSecureSessionStore implements SessionStore {
private static final String TAG = TextSecureSessionStore.class.getSimpleName();
private static final String SESSIONS_DIRECTORY_V2 = "sessions-v2";
private static final Object FILE_LOCK = new Object();
private static final int SINGLE_STATE_VERSION = 1;
private static final int ARCHIVE_STATES_VERSION = 2;
private static final int CURRENT_VERSION = 2;
private final Context context;
private final MasterSecret masterSecret;
public TextSecureSessionStore(Context context, MasterSecret masterSecret) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
}
@Override
public SessionRecord loadSession(long recipientId, int deviceId) {
synchronized (FILE_LOCK) {
try {
MasterCipher cipher = new MasterCipher(masterSecret);
FileInputStream in = new FileInputStream(getSessionFile(recipientId, deviceId));
int versionMarker = readInteger(in);
if (versionMarker > CURRENT_VERSION) {
throw new AssertionError("Unknown version: " + versionMarker);
}
byte[] serialized = cipher.decryptBytes(readBlob(in));
in.close();
if (versionMarker == SINGLE_STATE_VERSION) {
SessionStructure sessionStructure = SessionStructure.parseFrom(serialized);
SessionState sessionState = new SessionState(sessionStructure);
return new SessionRecord(sessionState);
} else if (versionMarker == ARCHIVE_STATES_VERSION) {
return new SessionRecord(serialized);
} else {
throw new AssertionError("Unknown version: " + versionMarker);
}
} catch (InvalidMessageException | IOException e) {
Log.w(TAG, "No existing session information found.");
return new SessionRecord();
}
}
}
@Override
public void storeSession(long recipientId, int deviceId, SessionRecord record) {
synchronized (FILE_LOCK) {
try {
MasterCipher masterCipher = new MasterCipher(masterSecret);
RandomAccessFile sessionFile = new RandomAccessFile(getSessionFile(recipientId, deviceId), "rw");
FileChannel out = sessionFile.getChannel();
out.position(0);
writeInteger(CURRENT_VERSION, out);
writeBlob(masterCipher.encryptBytes(record.serialize()), out);
out.truncate(out.position());
sessionFile.close();
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
@Override
public boolean containsSession(long recipientId, int deviceId) {
return getSessionFile(recipientId, deviceId).exists() &&
loadSession(recipientId, deviceId).getSessionState().hasSenderChain();
}
@Override
public void deleteSession(long recipientId, int deviceId) {
getSessionFile(recipientId, deviceId).delete();
}
@Override
public void deleteAllSessions(long recipientId) {
List<Integer> devices = getSubDeviceSessions(recipientId);
deleteSession(recipientId, RecipientDevice.DEFAULT_DEVICE_ID);
for (int device : devices) {
deleteSession(recipientId, device);
}
}
@Override
public List<Integer> getSubDeviceSessions(long recipientId) {
List<Integer> results = new LinkedList<>();
File parent = getSessionDirectory();
String[] children = parent.list();
if (children == null) return results;
for (String child : children) {
try {
String[] parts = child.split("[.]", 2);
long sessionRecipientId = Long.parseLong(parts[0]);
if (sessionRecipientId == recipientId && parts.length > 1) {
results.add(Integer.parseInt(parts[1]));
}
} catch (NumberFormatException e) {
Log.w("SessionRecordV2", e);
}
}
return results;
}
private File getSessionFile(long recipientId, int deviceId) {
return new File(getSessionDirectory(), getSessionName(recipientId, deviceId));
}
private File getSessionDirectory() {
File directory = new File(context.getFilesDir(), SESSIONS_DIRECTORY_V2);
if (!directory.exists()) {
if (!directory.mkdirs()) {
Log.w(TAG, "Session directory creation failed!");
}
}
return directory;
}
private String getSessionName(long recipientId, int deviceId) {
return recipientId + (deviceId == RecipientDevice.DEFAULT_DEVICE_ID ? "" : "." + deviceId);
}
private byte[] readBlob(FileInputStream in) throws IOException {
int length = readInteger(in);
byte[] blobBytes = new byte[length];
in.read(blobBytes, 0, blobBytes.length);
return blobBytes;
}
private void writeBlob(byte[] blobBytes, FileChannel out) throws IOException {
writeInteger(blobBytes.length, out);
out.write(ByteBuffer.wrap(blobBytes));
}
private int readInteger(FileInputStream in) throws IOException {
byte[] integer = new byte[4];
in.read(integer, 0, integer.length);
return Conversions.byteArrayToInt(integer);
}
private void writeInteger(int value, FileChannel out) throws IOException {
byte[] valueBytes = Conversions.intToByteArray(value);
out.write(ByteBuffer.wrap(valueBytes));
}
}
@@ -26,13 +26,12 @@ import android.util.Log;
import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Util;
@@ -467,7 +466,7 @@ public class DatabaseFactory {
db.setTransactionSuccessful();
db.endTransaction();
DecryptingQueue.schedulePendingDecrypts(context, masterSecret);
// DecryptingQueue.schedulePendingDecrypts(context, masterSecret);
MessageNotifier.updateNotification(context, masterSecret);
}
@@ -8,7 +8,7 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import java.util.LinkedList;
import java.util.List;
@@ -22,7 +22,7 @@ import android.util.Log;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import ws.com.google.android.mms.pdu.PduPart;
@@ -29,8 +29,8 @@ import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.LRUCache;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.lang.ref.SoftReference;
import java.util.Collections;
@@ -73,7 +73,9 @@ public class EncryptingSmsDatabase extends SmsDatabase {
{
long type = Types.BASE_INBOX_TYPE;
if (!message.isSecureMessage() && !message.isEndSession()) {
if (masterSecret == null && message.isSecureMessage()) {
type |= Types.ENCRYPTION_REMOTE_BIT;
} else {
type |= Types.ENCRYPTION_SYMMETRIC_BIT;
message = message.withMessageBody(getEncryptedBody(masterSecret, message.getMessageBody()));
}
@@ -97,8 +99,9 @@ public class EncryptingSmsDatabase extends SmsDatabase {
}
public void updateBundleMessageBody(MasterSecret masterSecret, long messageId, String body) {
updateMessageBodyAndType(messageId, body, Types.TOTAL_MASK,
Types.BASE_INBOX_TYPE | Types.ENCRYPTION_REMOTE_BIT | Types.SECURE_MESSAGE_BIT);
String encryptedBody = getEncryptedBody(masterSecret, body);
updateMessageBodyAndType(messageId, encryptedBody, Types.TOTAL_MASK,
Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT);
}
public void updateMessageBody(MasterSecret masterSecret, long messageId, String body) {
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
@@ -103,7 +104,7 @@ public class GroupDatabase extends Database {
}
public void create(byte[] groupId, String title, List<String> members,
AttachmentPointer avatar, String relay)
TextSecureAttachmentPointer avatar, String relay)
{
ContentValues contentValues = new ContentValues();
contentValues.put(GROUP_ID, GroupUtil.getEncodedId(groupId));
@@ -112,7 +113,7 @@ public class GroupDatabase extends Database {
if (avatar != null) {
contentValues.put(AVATAR_ID, avatar.getId());
contentValues.put(AVATAR_KEY, avatar.getKey().toByteArray());
contentValues.put(AVATAR_KEY, avatar.getKey());
contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType());
}
@@ -123,14 +124,14 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
}
public void update(byte[] groupId, String title, AttachmentPointer avatar) {
public void update(byte[] groupId, String title, TextSecureAttachmentPointer avatar) {
ContentValues contentValues = new ContentValues();
if (title != null) contentValues.put(TITLE, title);
if (avatar != null) {
contentValues.put(AVATAR_ID, avatar.getId());
contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType());
contentValues.put(AVATAR_KEY, avatar.getKey().toByteArray());
contentValues.put(AVATAR_KEY, avatar.getKey());
}
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
@@ -28,8 +28,8 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.util.Base64;
import java.io.IOException;
@@ -34,8 +34,8 @@ import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.DisplayRecord;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.LRUCache;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.util.InvalidNumberException;
import org.whispersystems.textsecure.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Trimmer;
@@ -429,6 +430,32 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
database.update(TABLE_NAME, contentValues, null, null);
}
public Optional<NotificationInd> getNotification(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, MMS_PROJECTION, ID_WHERE, new String[] {String.valueOf(messageId)}, null, null, null);
if (cursor != null && cursor.moveToNext()) {
PduHeaders headers = getHeadersFromCursor(cursor);
addressDatabase.getAddressesForId(messageId, headers);
return Optional.of(new NotificationInd(headers));
} else {
return Optional.absent();
}
} catch (InvalidHeaderValueException e) {
Log.w("MmsDatabase", e);
return Optional.absent();
} finally {
if (cursor != null)
cursor.close();
}
}
public SendReq[] getOutgoingMessages(MasterSecret masterSecret, long messageId)
throws MmsException
{
@@ -155,6 +155,10 @@ public interface MmsSmsColumns {
return (type & ENCRYPTION_SYMMETRIC_BIT) != 0;
}
public static boolean isAsymmetricEncryption(long type) {
return (type & ENCRYPTION_ASYMMETRIC_BIT) != 0;
}
public static boolean isFailedDecryptType(long type) {
return (type & ENCRYPTION_REMOTE_FAILED_BIT) != 0;
}
@@ -24,7 +24,7 @@ import android.database.sqlite.SQLiteQueryBuilder;
import android.util.Log;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.util.HashSet;
import java.util.Set;
@@ -4,7 +4,7 @@ package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.os.Environment;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import java.io.File;
@@ -6,8 +6,8 @@ import android.database.sqlite.SQLiteStatement;
import android.os.Environment;
import android.util.Log;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
@@ -4,6 +4,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.util.Base64;
@@ -40,6 +41,32 @@ public class PushDatabase extends Database {
return databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
}
public IncomingPushMessage get(long id) throws NoSuchMessageException {
Cursor cursor = null;
try {
cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, ID_WHERE,
new String[] {String.valueOf(id)},
null, null, null);
if (cursor != null && cursor.moveToNext()) {
return new IncomingPushMessage(cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)),
cursor.getString(cursor.getColumnIndexOrThrow(SOURCE)),
cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID)),
Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(BODY))),
cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)));
}
} catch (IOException e) {
Log.w("PushDatabase", e);
throw new NoSuchMessageException(e);
} finally {
if (cursor != null)
cursor.close();
}
throw new NoSuchMessageException("Not found");
}
public Cursor getPending() {
return databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null);
}
@@ -85,4 +112,9 @@ public class PushDatabase extends Database {
}
}
public static class NoSuchMessageException extends Exception {
public NoSuchMessageException(String s) {super(s);}
public NoSuchMessageException(Exception e) {super(e);}
}
}
@@ -350,7 +350,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
else if (((IncomingKeyExchangeMessage)message).isPreKeyBundle()) type |= Types.KEY_EXCHANGE_BUNDLE_BIT;
} else if (message.isSecureMessage()) {
type |= Types.SECURE_MESSAGE_BIT;
type |= Types.ENCRYPTION_REMOTE_BIT;
// type |= Types.ENCRYPTION_REMOTE_BIT;
} else if (message.isGroup()) {
type |= Types.SECURE_MESSAGE_BIT;
if (((IncomingGroupMessage)message).isUpdate()) type |= Types.GROUP_UPDATE_BIT;
@@ -358,7 +358,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
} else if (message.isEndSession()) {
type |= Types.SECURE_MESSAGE_BIT;
type |= Types.END_SESSION_BIT;
type |= Types.ENCRYPTION_REMOTE_BIT;
// type |= Types.ENCRYPTION_REMOTE_BIT;
}
if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT;
@@ -23,8 +23,8 @@ import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.util.Log;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
@@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.whispersystems.textsecure.util.Util;
import java.util.Arrays;
@@ -88,6 +88,10 @@ public abstract class MessageRecord extends DisplayRecord {
return MmsSmsColumns.Types.isLegacyType(type);
}
public boolean isAsymmetricEncryption() {
return MmsSmsColumns.Types.isAsymmetricEncryption(type);
}
@Override
public SpannableString getDisplayBody() {
if (isGroupUpdate() && isOutgoing()) {
@@ -8,20 +8,10 @@ import android.util.Log;
import com.google.android.gms.gcm.GoogleCloudMessaging;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.jobs.DeliveryReceiptJob;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.jobs.PushReceiveJob;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.IncomingEncryptedPushMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
public class GcmBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = GcmBroadcastReceiver.class.getSimpleName();
@@ -48,44 +38,8 @@ public class GcmBroadcastReceiver extends BroadcastReceiver {
}
private void handleReceivedMessage(Context context, String data) {
try {
String sessionKey = TextSecurePreferences.getSignalingKey(context);
IncomingEncryptedPushMessage encrypted = new IncomingEncryptedPushMessage(data, sessionKey);
IncomingPushMessage message = encrypted.getIncomingPushMessage();
if (!isActiveNumber(context, message.getSource())) {
Directory directory = Directory.getInstance(context);
ContactTokenDetails contactTokenDetails = new ContactTokenDetails();
contactTokenDetails.setNumber(message.getSource());
directory.setNumber(contactTokenDetails, true);
}
Intent receiveService = new Intent(context, SendReceiveService.class);
receiveService.setAction(SendReceiveService.RECEIVE_PUSH_ACTION);
receiveService.putExtra("message", message);
context.startService(receiveService);
if (!message.isReceipt()) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new DeliveryReceiptJob(context, message.getSource(),
message.getTimestampMillis(),
message.getRelay()));
}
} catch (IOException | InvalidVersionException e) {
Log.w(TAG, e);
}
}
private boolean isActiveNumber(Context context, String e164number) {
boolean isActiveNumber;
try {
isActiveNumber = Directory.getInstance(context).isActiveNumber(e164number);
} catch (NotInDirectoryException e) {
isActiveNumber = false;
}
return isActiveNumber;
ApplicationContext.getInstance(context)
.getJobManager()
.add(new PushReceiveJob(context, data));
}
}
@@ -0,0 +1,200 @@
package org.thoughtcrime.securesms.groups;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.util.Base64;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
public class GroupMessageProcessor {
private static final String TAG = GroupMessageProcessor.class.getSimpleName();
public static void process(Context context,
MasterSecret masterSecret,
IncomingPushMessage push,
TextSecureMessage message)
{
if (!message.getGroupInfo().isPresent() || message.getGroupInfo().get().getGroupId() == null) {
Log.w(TAG, "Received group message with no id! Ignoring...");
return;
}
if (!message.isSecure()) {
Log.w(TAG, "Received insecure group push action! Ignoring...");
return;
}
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
TextSecureGroup group = message.getGroupInfo().get();
byte[] id = group.getGroupId();
GroupRecord record = database.getGroup(id);
if (record != null && group.getType() == TextSecureGroup.Type.UPDATE) {
handleGroupUpdate(context, masterSecret, push, group, record);
} else if (record == null && group.getType() == TextSecureGroup.Type.UPDATE) {
handleGroupCreate(context, masterSecret, push, group);
} else if (record != null && group.getType() == TextSecureGroup.Type.QUIT) {
handleGroupLeave(context, masterSecret, push, group, record);
} else {
Log.w(TAG, "Received unknown type, ignoring...");
}
}
private static void handleGroupCreate(Context context,
MasterSecret masterSecret,
IncomingPushMessage message,
TextSecureGroup group)
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
byte[] id = group.getGroupId();
GroupContext.Builder builder = createGroupContext(group);
builder.setType(GroupContext.Type.UPDATE);
TextSecureAttachment avatar = group.getAvatar().orNull();
database.create(id, group.getName().orNull(), group.getMembers().orNull(),
avatar != null && avatar.isPointer() ? avatar.asPointer() : null,
message.getRelay());
storeMessage(context, masterSecret, message, group, builder.build());
}
private static void handleGroupUpdate(Context context,
MasterSecret masterSecret,
IncomingPushMessage push,
TextSecureGroup group,
GroupRecord groupRecord)
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
byte[] id = group.getGroupId();
Set<String> recordMembers = new HashSet<>(groupRecord.getMembers());
Set<String> messageMembers = new HashSet<>(group.getMembers().get());
Set<String> addedMembers = new HashSet<>(messageMembers);
addedMembers.removeAll(recordMembers);
Set<String> missingMembers = new HashSet<>(recordMembers);
missingMembers.removeAll(messageMembers);
GroupContext.Builder builder = createGroupContext(group);
builder.setType(GroupContext.Type.UPDATE);
if (addedMembers.size() > 0) {
Set<String> unionMembers = new HashSet<>(recordMembers);
unionMembers.addAll(messageMembers);
database.updateMembers(id, new LinkedList<>(unionMembers));
builder.clearMembers().addAllMembers(addedMembers);
} else {
builder.clearMembers();
}
if (missingMembers.size() > 0) {
// TODO We should tell added and missing about each-other.
}
if (group.getName().isPresent() || group.getAvatar().isPresent()) {
TextSecureAttachment avatar = group.getAvatar().orNull();
database.update(id, group.getName().orNull(), avatar != null ? avatar.asPointer() : null);
}
if (group.getName().isPresent() && group.getName().get().equals(groupRecord.getTitle())) {
builder.clearName();
}
if (!groupRecord.isActive()) database.setActive(id, true);
storeMessage(context, masterSecret, push, group, builder.build());
}
private static void handleGroupLeave(Context context,
MasterSecret masterSecret,
IncomingPushMessage message,
TextSecureGroup group,
GroupRecord record)
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
byte[] id = group.getGroupId();
List<String> members = record.getMembers();
GroupContext.Builder builder = createGroupContext(group);
builder.setType(GroupContext.Type.QUIT);
if (members.contains(message.getSource())) {
database.remove(id, message.getSource());
storeMessage(context, masterSecret, message, group, builder.build());
}
}
private static void storeMessage(Context context, MasterSecret masterSecret,
IncomingPushMessage push, TextSecureGroup group,
GroupContext storage)
{
if (group.getAvatar().isPresent()) {
ApplicationContext.getInstance(context).getJobManager()
.add(new AvatarDownloadJob(context, group.getGroupId()));
}
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = Base64.encodeBytes(storage.toByteArray());
IncomingTextMessage incoming = new IncomingTextMessage(push.getSource(), push.getSourceDevice(), push.getTimestampMillis(), body, Optional.of(group));
IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, storage, body);
Pair<Long, Long> messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, groupMessage);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private static GroupContext.Builder createGroupContext(TextSecureGroup group) {
GroupContext.Builder builder = GroupContext.newBuilder();
builder.setId(ByteString.copyFrom(group.getGroupId()));
if (group.getAvatar().isPresent() && group.getAvatar().get().isPointer()) {
builder.setAvatar(AttachmentPointer.newBuilder()
.setId(group.getAvatar().get().asPointer().getId())
.setKey(ByteString.copyFrom(group.getAvatar().get().asPointer().getKey()))
.setContentType(group.getAvatar().get().getContentType()));
}
if (group.getName().isPresent()) {
builder.setName(group.getName().get());
}
if (group.getMembers().isPresent()) {
builder.addAllMembers(group.getMembers().get());
}
return builder;
}
}
@@ -0,0 +1,154 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingPartDatabase;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.push.TextSecureMessageReceiverFactory;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.push.exceptions.PushNetworkException;
import org.whispersystems.textsecure.util.Base64;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduPart;
public class AttachmentDownloadJob extends MasterSecretJob {
private static final String TAG = AttachmentDownloadJob.class.getSimpleName();
private final long messageId;
public AttachmentDownloadJob(Context context, long messageId) {
super(context, JobParameters.newBuilder()
.withRequirement(new MasterSecretRequirement(context))
.withRequirement(new NetworkRequirement(context))
.withPersistence()
.create());
this.messageId = messageId;
}
@Override
public void onAdded() {}
@Override
public void onRun() throws RequirementNotMetException, IOException {
MasterSecret masterSecret = getMasterSecret();
PartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
Log.w(TAG, "Downloading push parts for: " + messageId);
List<Pair<Long, PduPart>> parts = database.getParts(messageId, false);
for (Pair<Long, PduPart> partPair : parts) {
retrievePart(masterSecret, partPair.second, messageId, partPair.first);
Log.w(TAG, "Got part: " + partPair.first);
}
}
@Override
public void onCanceled() {
try {
MasterSecret masterSecret = getMasterSecret();
PartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
List<Pair<Long, PduPart>> parts = database.getParts(messageId, false);
for (Pair<Long, PduPart> partPair : parts) {
markFailed(masterSecret, messageId, partPair.second, partPair.first);
}
} catch (RequirementNotMetException e) {
Log.w(TAG, e);
}
}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof PushNetworkException) return true;
if (throwable instanceof RequirementNotMetException) return true;
return false;
}
private void retrievePart(MasterSecret masterSecret, PduPart part, long messageId, long partId)
throws IOException
{
TextSecureMessageReceiver receiver = TextSecureMessageReceiverFactory.create(context, masterSecret);
EncryptingPartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
File attachmentFile = null;
try {
attachmentFile = createTempFile();
TextSecureAttachmentPointer pointer = createAttachmentPointer(masterSecret, part);
InputStream attachment = receiver.retrieveAttachment(pointer, attachmentFile);
database.updateDownloadedPart(messageId, partId, part, attachment);
} catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) {
Log.w(TAG, e);
markFailed(masterSecret, messageId, part, partId);
} finally {
if (attachmentFile != null)
attachmentFile.delete();
}
}
private TextSecureAttachmentPointer createAttachmentPointer(MasterSecret masterSecret, PduPart part)
throws InvalidPartException
{
try {
MasterCipher masterCipher = new MasterCipher(masterSecret);
long id = Long.parseLong(Util.toIsoString(part.getContentLocation()));
byte[] key = masterCipher.decryptBytes(Base64.decode(Util.toIsoString(part.getContentDisposition())));
String relay = null;
if (part.getName() != null) {
relay = Util.toIsoString(part.getName());
}
return new TextSecureAttachmentPointer(id, null, key, relay);
} catch (InvalidMessageException | IOException e) {
Log.w(TAG, e);
throw new InvalidPartException(e);
}
}
private File createTempFile() throws InvalidPartException {
try {
File file = File.createTempFile("push-attachment", "tmp");
file.deleteOnExit();
return file;
} catch (IOException e) {
throw new InvalidPartException(e);
}
}
private void markFailed(MasterSecret masterSecret, long messageId, PduPart part, long partId) {
try {
EncryptingPartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
database.updateFailedDownloadedPart(messageId, partId, part);
} catch (MmsException e) {
Log.w(TAG, e);
}
}
private class InvalidPartException extends Exception {
public InvalidPartException(Exception e) {super(e);}
}
}
@@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class AvatarDownloadJob extends MasterSecretJob {
private static final String TAG = AvatarDownloadJob.class.getSimpleName();
private final byte[] groupId;
public AvatarDownloadJob(Context context, byte[] groupId) {
super(context, JobParameters.newBuilder()
.withRequirement(new MasterSecretRequirement(context))
.withRequirement(new NetworkRequirement(context))
.withPersistence()
.create());
this.groupId = groupId;
}
@Override
public void onAdded() {}
@Override
public void onRun() throws IOException {
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
GroupDatabase.GroupRecord record = database.getGroup(groupId);
File attachment = null;
try {
if (record != null) {
long avatarId = record.getAvatarId();
byte[] key = record.getAvatarKey();
String relay = record.getRelay();
if (avatarId == -1 || key == null) {
return;
}
attachment = downloadAttachment(relay, avatarId);
InputStream scaleInputStream = new AttachmentCipherInputStream(attachment, key);
InputStream measureInputStream = new AttachmentCipherInputStream(attachment, key);
Bitmap avatar = BitmapUtil.createScaledBitmap(measureInputStream, scaleInputStream, 500, 500);
database.updateAvatar(groupId, avatar);
try {
Recipient groupRecipient = RecipientFactory.getRecipientsFromString(context, GroupUtil.getEncodedId(groupId), true)
.getPrimaryRecipient();
groupRecipient.setContactPhoto(avatar);
} catch (RecipientFormattingException e) {
Log.w("AvatarDownloader", e);
}
}
} catch (InvalidMessageException | BitmapDecodingException | NonSuccessfulResponseCodeException e) {
Log.w(TAG, e);
} finally {
if (attachment != null)
attachment.delete();
}
}
@Override
public void onCanceled() {}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof IOException) return true;
return false;
}
private File downloadAttachment(String relay, long contentLocation) throws IOException {
PushServiceSocket socket = PushServiceSocketFactory.create(context);
File destination = File.createTempFile("avatar", "tmp");
destination.deleteOnExit();
socket.retrieveAttachment(relay, contentLocation, destination);
return destination;
}
}
@@ -12,8 +12,8 @@ import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.whispersystems.textsecure.push.PushServiceSocket;
import java.io.IOException;
@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.whispersystems.jobqueue.JobParameters;
public abstract class MasterSecretJob extends ContextJob {
public MasterSecretJob(Context context, JobParameters parameters) {
super(context, parameters);
}
protected MasterSecret getMasterSecret() throws RequirementNotMetException {
MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
if (masterSecret == null) throw new RequirementNotMetException();
else return masterSecret;
}
protected static class RequirementNotMetException extends Exception {}
}
@@ -1,142 +1,135 @@
/**
* Copyright (C) 2013 Open 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.service;
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.mms.IncomingMmsConnection;
import org.thoughtcrime.securesms.mms.MmsConnection;
import org.thoughtcrime.securesms.mms.MmsConnection.Apn;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MmsCipher;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.IncomingMmsConnection;
import org.thoughtcrime.securesms.mms.MmsConnection;
import org.thoughtcrime.securesms.mms.MmsRadio;
import org.thoughtcrime.securesms.mms.MmsRadioException;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.io.IOException;
import ws.com.google.android.mms.InvalidHeaderValueException;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.NotifyRespInd;
import ws.com.google.android.mms.pdu.PduComposer;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.RetrieveConf;
public class MmsDownloader {
import static org.thoughtcrime.securesms.mms.MmsConnection.Apn;
private final Context context;
private final SendReceiveService.ToastHandler toastHandler;
private final MmsRadio radio;
public class MmsDownloadJob extends MasterSecretJob {
public MmsDownloader(Context context, SendReceiveService.ToastHandler toastHandler) {
this.context = context;
this.toastHandler = toastHandler;
this.radio = MmsRadio.getInstance(context);
private static final String TAG = MmsDownloadJob.class.getSimpleName();
private final long messageId;
private final long threadId;
private final boolean automatic;
public MmsDownloadJob(Context context, long messageId, long threadId, boolean automatic) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.withRequirement(new NetworkRequirement(context))
.withGroupId("mms-download")
.create());
this.messageId = messageId;
this.threadId = threadId;
this.automatic = automatic;
}
public void process(MasterSecret masterSecret, Intent intent) {
if (SendReceiveService.DOWNLOAD_MMS_ACTION.equals(intent.getAction())) {
handleDownloadMms(masterSecret, intent);
} else if (SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION.equals(intent.getAction())) {
handleMmsPendingApnDownloads(masterSecret);
@Override
public void onAdded() {
if (automatic && KeyCachingService.getMasterSecret(context) == null) {
DatabaseFactory.getMmsDatabase(context).markIncomingNotificationReceived(threadId);
MessageNotifier.updateNotification(context, null);
}
}
private void handleMmsPendingApnDownloads(MasterSecret masterSecret) {
if (!IncomingMmsConnection.isConnectionPossible(context, null))
@Override
public void onRun() throws RequirementNotMetException {
Log.w(TAG, "MmsDownloadJob:onRun()");
MasterSecret masterSecret = getMasterSecret();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Optional<NotificationInd> notification = database.getNotification(messageId);
if (!notification.isPresent()) {
Log.w(TAG, "No notification for ID: " + messageId);
return;
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
MmsDatabase.Reader stalledMmsReader = mmsDatabase.getNotificationsWithDownloadState(masterSecret,
MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE);
while (stalledMmsReader.getNext() != null) {
NotificationMmsMessageRecord stalledMmsRecord = (NotificationMmsMessageRecord) stalledMmsReader.getCurrent();
Intent intent = new Intent(SendReceiveService.DOWNLOAD_MMS_ACTION, null, context, SendReceiveService.class);
intent.putExtra("content_location", new String(stalledMmsRecord.getContentLocation()));
intent.putExtra("message_id", stalledMmsRecord.getId());
intent.putExtra("transaction_id", stalledMmsRecord.getTransactionId());
intent.putExtra("thread_id", stalledMmsRecord.getThreadId());
intent.putExtra("automatic", true);
context.startService(intent);
}
stalledMmsReader.close();
}
private void handleDownloadMms(MasterSecret masterSecret, Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
long threadId = intent.getLongExtra("thread_id", -1);
byte[] transactionId = intent.getByteArrayExtra("transaction_id");
String contentLocation = intent.getStringExtra("content_location");
boolean automatic = intent.getBooleanExtra("automatic", false);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_CONNECTING);
Log.w("MmsDownloader", "Downloading mms at "+ Uri.parse(contentLocation).getHost());
String contentLocation = new String(notification.get().getContentLocation());
byte[] transactionId = notification.get().getTransactionId();
MmsRadio radio = MmsRadio.getInstance(context);
Log.w(TAG, "About to parse URL...");
Log.w(TAG, "Downloading mms at " + Uri.parse(contentLocation).getHost());
try {
if (isCdmaNetwork()) {
Log.w("MmsDownloader", "Connecting directly...");
Log.w(TAG, "Connecting directly...");
try {
retrieveAndStore(masterSecret, messageId, threadId, contentLocation,
retrieveAndStore(masterSecret, radio, messageId, threadId, contentLocation,
transactionId, false, false);
return;
} catch (IOException e) {
Log.w("MmsDownloader", e);
Log.w(TAG, e);
}
}
Log.w("MmsDownloader", "Changing radio to MMS mode..");
Log.w(TAG, "Changing radio to MMS mode..");
radio.connect();
Log.w("MmsDownloader", "Downloading in MMS mode with proxy...");
Log.w(TAG, "Downloading in MMS mode with proxy...");
try {
retrieveAndStore(masterSecret, messageId, threadId, contentLocation,
retrieveAndStore(masterSecret, radio, messageId, threadId, contentLocation,
transactionId, true, true);
radio.disconnect();
return;
} catch (IOException e) {
Log.w("MmsDownloader", e);
Log.w(TAG, e);
}
Log.w("MmsDownloader", "Downloading in MMS mode without proxy...");
Log.w(TAG, "Downloading in MMS mode without proxy...");
try {
retrieveAndStore(masterSecret, messageId, threadId,
retrieveAndStore(masterSecret, radio, messageId, threadId,
contentLocation, transactionId, true, false);
radio.disconnect();
} catch (IOException e) {
Log.w("MmsDownloader", e);
Log.w(TAG, e);
radio.disconnect();
handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
@@ -145,28 +138,53 @@ public class MmsDownloader {
}
} catch (ApnUnavailableException e) {
Log.w("MmsDownloader", e);
Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId, MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE,
context.getString(R.string.MmsDownloader_error_reading_mms_settings), automatic);
} catch (MmsException e) {
Log.w("MmsDownloader", e);
Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_HARD_FAILURE,
context.getString(R.string.MmsDownloader_error_storing_mms),
automatic);
} catch (MmsRadioException e) {
Log.w("MmsDownloader", e);
Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider),
automatic);
} catch (DuplicateMessageException e) {
Log.w(TAG, e);
database.markAsDecryptDuplicate(messageId, threadId);
} catch (LegacyMessageException e) {
Log.w(TAG, e);
database.markAsLegacyVersion(messageId, threadId);
} catch (NoSessionException e) {
Log.w(TAG, e);
database.markAsNoSession(messageId, threadId);
} catch (InvalidMessageException e) {
Log.w(TAG, e);
database.markAsDecryptFailed(messageId, threadId);
}
}
private void retrieveAndStore(MasterSecret masterSecret, long messageId, long threadId,
@Override
public void onCanceled() {
// TODO
}
@Override
public boolean onShouldRetry(Throwable throwable) {
return false;
}
private void retrieveAndStore(MasterSecret masterSecret, MmsRadio radio,
long messageId, long threadId,
String contentLocation, byte[] transactionId,
boolean radioEnabled, boolean useProxy)
throws IOException, MmsException, ApnUnavailableException
throws IOException, MmsException, ApnUnavailableException,
DuplicateMessageException, NoSessionException,
InvalidMessageException, LegacyMessageException
{
Apn dbApn = MmsConnection.getApn(context, radio.getApnInformation());
Apn contentApn = new Apn(contentLocation, dbApn.getProxy(), Integer.toString(dbApn.getPort()));
@@ -174,12 +192,13 @@ public class MmsDownloader {
RetrieveConf retrieved = connection.retrieve(radioEnabled, useProxy);
storeRetrievedMms(masterSecret, contentLocation, messageId, threadId, retrieved);
sendRetrievedAcknowledgement(transactionId, radioEnabled, useProxy);
sendRetrievedAcknowledgement(radio, transactionId, radioEnabled, useProxy);
}
private void storeRetrievedMms(MasterSecret masterSecret, String contentLocation,
long messageId, long threadId, RetrieveConf retrieved)
throws MmsException
throws MmsException, NoSessionException, DuplicateMessageException, InvalidMessageException,
LegacyMessageException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
IncomingMediaMessage message = new IncomingMediaMessage(retrieved);
@@ -187,12 +206,17 @@ public class MmsDownloader {
Pair<Long, Long> messageAndThreadId;
if (retrieved.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieved.getSubject().getString())) {
messageAndThreadId = database.insertSecureMessageInbox(masterSecret, message,
contentLocation, threadId);
MmsCipher mmsCipher = new MmsCipher(new TextSecureAxolotlStore(context, masterSecret));
MultimediaMessagePdu plaintextPdu = mmsCipher.decrypt(context, retrieved);
RetrieveConf plaintextRetrieved = new RetrieveConf(plaintextPdu.getPduHeaders(), plaintextPdu.getBody());
IncomingMediaMessage plaintextMessage = new IncomingMediaMessage(plaintextRetrieved);
if (masterSecret != null)
DecryptingQueue.scheduleDecryption(context, masterSecret, messageAndThreadId.first,
messageAndThreadId.second, retrieved);
messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, plaintextMessage,
threadId);
// if (masterSecret != null)
// DecryptingQueue.scheduleDecryption(context, masterSecret, messageAndThreadId.first,
// messageAndThreadId.second, retrieved);
} else {
messageAndThreadId = database.insertMessageInbox(masterSecret, message,
@@ -203,7 +227,8 @@ public class MmsDownloader {
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void sendRetrievedAcknowledgement(byte[] transactionId,
private void sendRetrievedAcknowledgement(MmsRadio radio,
byte[] transactionId,
boolean usingRadio,
boolean useProxy)
throws ApnUnavailableException
@@ -215,14 +240,11 @@ public class MmsDownloader {
OutgoingMmsConnection connection = new OutgoingMmsConnection(context, radio.getApnInformation(), new PduComposer(context, notifyResponse).make());
connection.sendNotificationReceived(usingRadio, useProxy);
} catch (InvalidHeaderValueException e) {
Log.w("MmsDownloader", e);
} catch (IOException e) {
Log.w("MmsDownloader", e);
} catch (InvalidHeaderValueException | IOException e) {
Log.w(TAG, e);
}
}
private void handleDownloadError(MasterSecret masterSecret, long messageId, long threadId,
int downloadStatus, String error, boolean automatic)
{
@@ -234,8 +256,8 @@ public class MmsDownloader {
db.markIncomingNotificationReceived(threadId);
MessageNotifier.updateNotification(context, masterSecret, threadId);
}
toastHandler.makeToast(error);
//
// toastHandler.makeToast(error);
}
private boolean isCdmaNetwork() {
@@ -243,4 +265,5 @@ public class MmsDownloader {
.getSystemService(Context.TELEPHONY_SERVICE))
.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
}
}
@@ -0,0 +1,69 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.whispersystems.jobqueue.JobParameters;
import ws.com.google.android.mms.pdu.GenericPdu;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.PduParser;
public class MmsReceiveJob extends ContextJob {
private static final String TAG = MmsReceiveJob.class.getSimpleName();
private final byte[] data;
public MmsReceiveJob(Context context, byte[] data) {
super(context, JobParameters.newBuilder()
.withPersistence().create());
this.data = data;
}
@Override
public void onAdded() {
}
@Override
public void onRun() {
if (data == null) {
Log.w(TAG, "Received NULL pdu, ignoring...");
return;
}
PduParser parser = new PduParser(data);
GenericPdu pdu = parser.parse();
if (pdu != null && pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox((NotificationInd)pdu);
Log.w(TAG, "Inserted received MMS notification...");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MmsDownloadJob(context,
messageAndThreadId.first,
messageAndThreadId.second,
true));
}
}
@Override
public void onCanceled() {
// TODO
}
@Override
public boolean onShouldRetry(Throwable throwable) {
return false;
}
}
@@ -0,0 +1,252 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.push.TextSecureMessageReceiverFactory;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.util.Base64;
import ws.com.google.android.mms.MmsException;
public class PushDecryptJob extends MasterSecretJob {
public static final String TAG = PushDecryptJob.class.getSimpleName();
private final long messageId;
public PushDecryptJob(Context context, long messageId) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.create());
this.messageId = messageId;
}
@Override
public void onAdded() {
if (KeyCachingService.getMasterSecret(context) == null) {
MessageNotifier.updateNotification(context, null);
}
}
@Override
public void onRun() throws RequirementNotMetException {
try {
MasterSecret masterSecret = getMasterSecret();
PushDatabase database = DatabaseFactory.getPushDatabase(context);
IncomingPushMessage push = database.get(messageId);
handleMessage(masterSecret, push);
database.delete(messageId);
} catch (PushDatabase.NoSuchMessageException e) {
Log.w(TAG, e);
}
}
@Override
public void onCanceled() {
}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof RequirementNotMetException) return true;
return false;
}
private void handleMessage(MasterSecret masterSecret, IncomingPushMessage push) {
try {
Recipients recipients = RecipientFactory.getRecipientsFromMessage(context, push, false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
TextSecureMessageReceiver messageReceiver = TextSecureMessageReceiverFactory.create(context, masterSecret);
TextSecureMessage message = messageReceiver.receiveMessage(recipientId, push);
if (message.isEndSession()) handleEndSessionMessage(masterSecret, recipientId, push, message);
else if (message.isGroupUpdate()) handleGroupMessage(masterSecret, push, message);
else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, push, message);
else handleTextMessage(masterSecret, push, message);
} catch (InvalidVersionException e) {
Log.w(TAG, e);
handleInvalidVersionMessage(masterSecret, push);
} catch (InvalidMessageException | InvalidKeyIdException | InvalidKeyException | MmsException e) {
Log.w(TAG, e);
handleCorruptMessage(masterSecret, push);
} catch (NoSessionException e) {
Log.w(TAG, e);
handleNoSessionMessage(masterSecret, push);
} catch (LegacyMessageException e) {
Log.w(TAG, e);
handleLegacyMessage(masterSecret, push);
} catch (DuplicateMessageException e) {
Log.w(TAG, e);
handleDuplicateMessage(masterSecret, push);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
handleUntrustedIdentityMessage(masterSecret, push);
}
}
private void handleEndSessionMessage(MasterSecret masterSecret, long recipientId, IncomingPushMessage push, TextSecureMessage message) {
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(push.getSource(),
push.getSourceDevice(),
message.getTimestamp(),
"", Optional.<TextSecureGroup>absent());
IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage);
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, incomingEndSessionMessage);
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
sessionStore.deleteAllSessions(recipientId);
SecurityEvent.broadcastSecurityUpdateEvent(context, messageAndThreadId.second);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleGroupMessage(MasterSecret masterSecret, IncomingPushMessage push, TextSecureMessage message) {
GroupMessageProcessor.process(context, masterSecret, push, message);
}
private void handleMediaMessage(MasterSecret masterSecret, IncomingPushMessage signal, TextSecureMessage message)
throws MmsException
{
String localNumber = TextSecurePreferences.getLocalNumber(context);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, signal.getSource(),
localNumber, message.getTimestamp(),
Optional.fromNullable(signal.getRelay()),
message.getBody(),
message.getGroupInfo(),
message.getAttachments());
Pair<Long, Long> messageAndThreadId;
if (message.isSecure()) {
messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
} else {
messageAndThreadId = database.insertMessageInbox(masterSecret, mediaMessage, null, -1);
}
ApplicationContext.getInstance(context)
.getJobManager()
.add(new AttachmentDownloadJob(context, messageAndThreadId.first));
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleTextMessage(MasterSecret masterSecret, IncomingPushMessage signal, TextSecureMessage message) {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : "";
IncomingTextMessage textMessage = new IncomingTextMessage(signal.getSource(),
signal.getSourceDevice(),
message.getTimestamp(), body,
message.getGroupInfo());
if (message.isSecure()) {
textMessage = new IncomingEncryptedMessage(textMessage, body);
}
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
// database.updateMessageBody(masterSecret, messageAndThreadId.first, body);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleInvalidVersionMessage(MasterSecret masterSecret, IncomingPushMessage push) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, push);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsInvalidVersionKeyExchange(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleCorruptMessage(MasterSecret masterSecret, IncomingPushMessage push) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, push);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptFailed(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleNoSessionMessage(MasterSecret masterSecret, IncomingPushMessage push) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, push);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsNoSession(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleLegacyMessage(MasterSecret masterSecret, IncomingPushMessage push) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, push);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsLegacyVersion(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleDuplicateMessage(MasterSecret masterSecret, IncomingPushMessage push) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, push);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptDuplicate(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleUntrustedIdentityMessage(MasterSecret masterSecret, IncomingPushMessage push) {
String encoded = Base64.encodeBytes(push.getBody());
IncomingTextMessage textMessage = new IncomingTextMessage(push.getSource(), push.getSourceDevice(),
push.getTimestampMillis(), encoded,
Optional.<TextSecureGroup>absent());
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
Pair<Long, Long> messageAndThreadId = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageInbox(masterSecret, bundleMessage);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private Pair<Long, Long> insertPlaceholder(MasterSecret masterSecret, IncomingPushMessage push) {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(push.getSource(), push.getSourceDevice(),
push.getTimestampMillis(), "",
Optional.<TextSecureGroup>absent());
textMessage = new IncomingEncryptedMessage(textMessage, "");
return database.insertMessageInbox(masterSecret, textMessage);
}
}
@@ -0,0 +1,98 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.IncomingEncryptedPushMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import java.io.IOException;
public class PushReceiveJob extends ContextJob {
private static final String TAG = PushReceiveJob.class.getSimpleName();
private final String data;
public PushReceiveJob(Context context, String data) {
super(context, JobParameters.newBuilder()
.withPersistence()
.create());
this.data = data;
}
@Override
public void onAdded() {}
@Override
public void onRun() {
try {
String sessionKey = TextSecurePreferences.getSignalingKey(context);
IncomingEncryptedPushMessage encrypted = new IncomingEncryptedPushMessage(data, sessionKey);
IncomingPushMessage message = encrypted.getIncomingPushMessage();
if (!isActiveNumber(context, message.getSource())) {
Directory directory = Directory.getInstance(context);
ContactTokenDetails contactTokenDetails = new ContactTokenDetails();
contactTokenDetails.setNumber(message.getSource());
directory.setNumber(contactTokenDetails, true);
}
if (message.isReceipt()) handleReceipt(message);
else handleMessage(message);
} catch (IOException | InvalidVersionException e) {
Log.w(TAG, e);
}
}
@Override
public void onCanceled() {
}
@Override
public boolean onShouldRetry(Throwable throwable) {
return false;
}
private void handleMessage(IncomingPushMessage message) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
long messageId = DatabaseFactory.getPushDatabase(context).insert(message);
jobManager.add(new DeliveryReceiptJob(context, message.getSource(),
message.getTimestampMillis(),
message.getRelay()));
jobManager.add(new PushDecryptJob(context, messageId));
}
private void handleReceipt(IncomingPushMessage message) {
Log.w(TAG, String.format("Received receipt: (XXXXX, %d)", message.getTimestampMillis()));
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(message.getSource(),
message.getTimestampMillis());
}
private boolean isActiveNumber(Context context, String e164number) {
boolean isActiveNumber;
try {
isActiveNumber = Directory.getInstance(context).isActiveNumber(e164number);
} catch (NotInDirectoryException e) {
isActiveNumber = false;
}
return isActiveNumber;
}
}
@@ -0,0 +1,220 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.crypto.SmsCipher;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.StaleKeyExchangeException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import java.io.IOException;
public class SmsDecryptJob extends MasterSecretJob {
private static final String TAG = SmsDecryptJob.class.getSimpleName();
private final long messageId;
public SmsDecryptJob(Context context, long messageId) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.create());
this.messageId = messageId;
}
@Override
public void onAdded() {
if (KeyCachingService.getMasterSecret(context) == null) {
MessageNotifier.updateNotification(context, null);
}
}
@Override
public void onRun() throws RequirementNotMetException {
MasterSecret masterSecret = getMasterSecret();
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsDatabase.Reader reader = null;
try {
reader = database.getMessage(masterSecret, messageId);
SmsMessageRecord record = reader.getNext();
IncomingTextMessage message = createIncomingTextMessage(masterSecret, record);
long messageId = record.getId();
long threadId = record.getThreadId();
if (message.isSecureMessage()) handleSecureMessage(masterSecret, messageId, message);
else if (message.isPreKeyBundle()) handlePreKeyWhisperMessage(masterSecret, messageId, threadId, (IncomingPreKeyBundleMessage) message);
else if (message.isKeyExchange()) handleKeyExchangeMessage(masterSecret, messageId, threadId, (IncomingKeyExchangeMessage) message);
else if (message.isEndSession()) handleSecureMessage(masterSecret, messageId, message);
else database.updateMessageBody(masterSecret, messageId, message.getMessageBody());
MessageNotifier.updateNotification(context, masterSecret);
} catch (LegacyMessageException e) {
Log.w(TAG, e);
database.markAsLegacyVersion(messageId);
} catch (InvalidMessageException e) {
Log.w(TAG, e);
database.markAsDecryptFailed(messageId);
} catch (DuplicateMessageException e) {
Log.w(TAG, e);
database.markAsDecryptDuplicate(messageId);
} catch (NoSessionException e) {
Log.w(TAG, e);
database.markAsNoSession(messageId);
} finally {
if (reader != null)
reader.close();
}
}
@Override
public void onCanceled() {
// TODO
}
private void handleSecureMessage(MasterSecret masterSecret, long messageId, IncomingTextMessage message)
throws NoSessionException, DuplicateMessageException,
InvalidMessageException, LegacyMessageException
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsCipher cipher = new SmsCipher(new TextSecureAxolotlStore(context, masterSecret));
IncomingTextMessage plaintext = cipher.decrypt(context, message);
database.updateMessageBody(masterSecret, messageId, plaintext.getMessageBody());
}
private void handlePreKeyWhisperMessage(MasterSecret masterSecret, long messageId, long threadId,
IncomingPreKeyBundleMessage message)
throws NoSessionException, DuplicateMessageException,
InvalidMessageException, LegacyMessageException
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
try {
SmsCipher smsCipher = new SmsCipher(new TextSecureAxolotlStore(context, masterSecret));
IncomingEncryptedMessage plaintext = smsCipher.decrypt(context, message);
database.updateBundleMessageBody(masterSecret, messageId, plaintext.getMessageBody());
SecurityEvent.broadcastSecurityUpdateEvent(context, threadId);
} catch (InvalidVersionException e) {
Log.w(TAG, e);
database.markAsInvalidVersionKeyExchange(messageId);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
}
}
private void handleKeyExchangeMessage(MasterSecret masterSecret, long messageId, long threadId,
IncomingKeyExchangeMessage message)
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
if (TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) {
try {
SmsCipher cipher = new SmsCipher(new TextSecureAxolotlStore(context, masterSecret));
OutgoingKeyExchangeMessage response = cipher.process(context, message);
database.markAsProcessedKeyExchange(messageId);
if (response != null) {
MessageSender.send(context, masterSecret, response, threadId, true);
}
} catch (InvalidVersionException e) {
Log.w(TAG, e);
database.markAsInvalidVersionKeyExchange(messageId);
} catch (InvalidMessageException e) {
Log.w(TAG, e);
database.markAsCorruptKeyExchange(messageId);
} catch (LegacyMessageException e) {
Log.w(TAG, e);
database.markAsLegacyVersion(messageId);
} catch (StaleKeyExchangeException e) {
Log.w(TAG, e);
database.markAsStaleKeyExchange(messageId);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
}
}
}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof RequirementNotMetException) return true;
return false;
}
private String getAsymmetricDecryptedBody(MasterSecret masterSecret, String body)
throws InvalidMessageException
{
try {
AsymmetricMasterSecret asymmetricMasterSecret = MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret);
AsymmetricMasterCipher asymmetricMasterCipher = new AsymmetricMasterCipher(asymmetricMasterSecret);
return asymmetricMasterCipher.decryptBody(body);
} catch (IOException e) {
throw new InvalidMessageException(e);
}
}
private IncomingTextMessage createIncomingTextMessage(MasterSecret masterSecret, SmsMessageRecord record)
throws InvalidMessageException
{
String plaintextBody = record.getBody().getBody();
if (record.isAsymmetricEncryption()) {
plaintextBody = getAsymmetricDecryptedBody(masterSecret, record.getBody().getBody());
}
IncomingTextMessage message = new IncomingTextMessage(record.getRecipients().getPrimaryRecipient().getNumber(),
record.getRecipientDeviceId(),
record.getDateSent(),
plaintextBody,
Optional.<TextSecureGroup>absent());
if (record.isEndSession()) {
return new IncomingEndSessionMessage(message);
} else if (record.isBundleKeyExchange()) {
return new IncomingPreKeyBundleMessage(message, message.getMessageBody());
} else if (record.isKeyExchange()) {
return new IncomingKeyExchangeMessage(message, message.getMessageBody());
} else if (record.isSecure()) {
return new IncomingEncryptedMessage(message, message.getMessageBody());
}
return message;
}
}
@@ -0,0 +1,110 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.telephony.SmsMessage;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.LinkedList;
import java.util.List;
public class SmsReceiveJob extends ContextJob {
private static final String TAG = SmsReceiveJob.class.getSimpleName();
private static MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
private final Object[] pdus;
public SmsReceiveJob(Context context, Object[] pdus) {
super(context, JobParameters.newBuilder()
.withPersistence()
.create());
this.pdus = pdus;
}
@Override
public void onAdded() {}
@Override
public void onRun() {
Optional<IncomingTextMessage> message = assembleMessageFragments(pdus);
if (message.isPresent()) {
Pair<Long, Long> messageAndThreadId = storeMessage(message.get());
MessageNotifier.updateNotification(context, KeyCachingService.getMasterSecret(context), messageAndThreadId.second);
}
}
@Override
public void onCanceled() {
}
@Override
public boolean onShouldRetry(Throwable throwable) {
return false;
}
private Pair<Long, Long> storeMessage(IncomingTextMessage message) {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
Pair<Long, Long> messageAndThreadId;
if (message.isSecureMessage()) {
messageAndThreadId = database.insertMessageInbox((MasterSecret)null, message);
} else if (masterSecret == null) {
messageAndThreadId = database.insertMessageInbox(MasterSecretUtil.getAsymmetricMasterSecret(context, null), message);
} else {
messageAndThreadId = database.insertMessageInbox(masterSecret, message);
}
if (masterSecret == null || message.isSecureMessage() || message.isKeyExchange()) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new SmsDecryptJob(context, messageAndThreadId.first));
} else {
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
return messageAndThreadId;
}
private Optional<IncomingTextMessage> assembleMessageFragments(Object[] pdus) {
List<IncomingTextMessage> messages = new LinkedList<>();
for (Object pdu : pdus) {
messages.add(new IncomingTextMessage(SmsMessage.createFromPdu((byte[])pdu)));
}
if (messages.isEmpty()) {
return Optional.absent();
}
IncomingTextMessage message = new IncomingTextMessage(messages);
if (WirePrefix.isEncryptedMessage(message.getMessageBody()) ||
WirePrefix.isKeyExchange(message.getMessageBody()) ||
WirePrefix.isPreKeyBundle(message.getMessageBody()) ||
WirePrefix.isEndSession(message.getMessageBody()))
{
return Optional.fromNullable(multipartMessageHandler.processPotentialMultipartMessage(message));
} else {
return Optional.of(message);
}
}
}
@@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ParcelUtil;
import org.whispersystems.jobqueue.EncryptionKeys;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
public class SmsSendJob extends ContextJob {
private transient MasterSecret masterSecret;
private final long messageId;
public SmsSendJob(Context context, MasterSecret masterSecret, long messageId, String name) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withEncryption(new EncryptionKeys(ParcelUtil.serialize(masterSecret)))
.withGroupId(name)
.create());
this.messageId = messageId;
}
@Override
public void onAdded() {
}
@Override
public void onRun() {
MasterSecret masterSecret = ParcelUtil.deserialize(getEncryptionKeys().getEncoded(), MasterSecret.CREATOR);
}
@Override
public void onCanceled() {
}
@Override
public boolean onShouldRetry(Throwable throwable) {
return false;
}
}
@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.jobs;
package org.thoughtcrime.securesms.jobs.persistence;
import android.content.Context;
@@ -8,8 +8,8 @@ import org.whispersystems.jobqueue.Job;
import org.whispersystems.jobqueue.persistence.JavaJobSerializer;
import org.whispersystems.jobqueue.persistence.JobSerializer;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.io.IOException;
@@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.jobs.requirements;
import android.content.Context;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.whispersystems.jobqueue.dependencies.ContextDependent;
import org.whispersystems.jobqueue.requirements.Requirement;
public class MasterSecretRequirement implements Requirement, ContextDependent {
private transient Context context;
public MasterSecretRequirement(Context context) {
this.context = context;
}
@Override
public boolean isPresent() {
return KeyCachingService.getMasterSecret(context) != null;
}
@Override
public void setContext(Context context) {
this.context = context;
}
}
@@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.jobs.requirements;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.whispersystems.jobqueue.requirements.RequirementListener;
import org.whispersystems.jobqueue.requirements.RequirementProvider;
public class MasterSecretRequirementProvider implements RequirementProvider {
private final BroadcastReceiver newKeyReceiver;
private RequirementListener listener;
public MasterSecretRequirementProvider(Context context) {
this.newKeyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (listener != null) {
listener.onRequirementStatusChanged();
}
}
};
IntentFilter filter = new IntentFilter(KeyCachingService.NEW_KEY_EVENT);
context.registerReceiver(newKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
}
@Override
public void setListener(RequirementListener listener) {
this.listener = listener;
}
}
@@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.util.SmilUtil;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -2,12 +2,16 @@ package org.thoughtcrime.securesms.mms;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Hex;
import java.util.List;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
@@ -30,49 +34,56 @@ public class IncomingMediaMessage {
this.push = false;
}
public IncomingMediaMessage(MasterSecret masterSecret, String localNumber,
IncomingPushMessage message,
PushMessageContent messageContent)
public IncomingMediaMessage(MasterSecret masterSecret,
String from,
String to,
long sentTimeMillis,
Optional<String> relay,
Optional<String> body,
Optional<TextSecureGroup> group,
Optional<List<TextSecureAttachment>> attachments)
{
this.headers = new PduHeaders();
this.body = new PduBody();
this.push = true;
if (messageContent.hasGroup()) {
this.groupId = GroupUtil.getEncodedId(messageContent.getGroup().getId().toByteArray());
if (group.isPresent()) {
this.groupId = GroupUtil.getEncodedId(group.get().getGroupId());
} else {
this.groupId = null;
}
this.headers.setEncodedStringValue(new EncodedStringValue(message.getSource()), PduHeaders.FROM);
this.headers.appendEncodedStringValue(new EncodedStringValue(localNumber), PduHeaders.TO);
this.headers.setLongInteger(message.getTimestampMillis() / 1000, PduHeaders.DATE);
this.headers.setEncodedStringValue(new EncodedStringValue(from), PduHeaders.FROM);
this.headers.appendEncodedStringValue(new EncodedStringValue(to), PduHeaders.TO);
this.headers.setLongInteger(sentTimeMillis / 1000, PduHeaders.DATE);
if (!org.whispersystems.textsecure.util.Util.isEmpty(messageContent.getBody())) {
if (body.isPresent() && !org.whispersystems.textsecure.util.Util.isEmpty(body.get())) {
PduPart text = new PduPart();
text.setData(Util.toUtf8Bytes(messageContent.getBody()));
text.setData(Util.toUtf8Bytes(body.get()));
text.setContentType(Util.toIsoBytes("text/plain"));
text.setCharset(CharacterSets.UTF_8);
body.addPart(text);
this.body.addPart(text);
}
if (messageContent.getAttachmentsCount() > 0) {
for (PushMessageContent.AttachmentPointer attachment : messageContent.getAttachmentsList()) {
PduPart media = new PduPart();
byte[] encryptedKey = new MasterCipher(masterSecret).encryptBytes(attachment.getKey().toByteArray());
if (attachments.isPresent()) {
for (TextSecureAttachment attachment : attachments.get()) {
if (attachment.isPointer()) {
PduPart media = new PduPart();
byte[] encryptedKey = new MasterCipher(masterSecret).encryptBytes(attachment.asPointer().getKey());
media.setContentType(Util.toIsoBytes(attachment.getContentType()));
media.setContentLocation(Util.toIsoBytes(String.valueOf(attachment.getId())));
media.setContentDisposition(Util.toIsoBytes(Base64.encodeBytes(encryptedKey)));
media.setContentType(Util.toIsoBytes(attachment.getContentType()));
media.setContentLocation(Util.toIsoBytes(String.valueOf(attachment.asPointer().getId())));
media.setContentDisposition(Util.toIsoBytes(Base64.encodeBytes(encryptedKey)));
if (message.getRelay() != null) {
media.setName(Util.toIsoBytes(message.getRelay()));
if (relay.isPresent()) {
media.setName(Util.toIsoBytes(relay.get()));
}
media.setPendingPush(true);
this.body.addPart(media);
}
media.setPendingPush(true);
body.addPart(media);
}
}
}
@@ -99,6 +99,8 @@ public class MmsRadio {
private boolean isConnected() {
NetworkInfo info = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
Log.w("MmsRadio", "Connected: " + info);
if ((info == null) || (info.getType() != TYPE_MOBILE_MMS) || !info.isConnected())
return false;
@@ -23,7 +23,7 @@ import java.io.InputStream;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.providers.PartProvider;
@@ -20,7 +20,7 @@ import android.content.Context;
import org.thoughtcrime.securesms.dom.smil.parser.SmilXmlSerializer;
import org.thoughtcrime.securesms.util.SmilUtil;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
@@ -24,7 +24,7 @@ import org.thoughtcrime.securesms.util.SmilUtil;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.LRUCache;
import java.io.UnsupportedEncodingException;
@@ -7,7 +7,7 @@ import android.content.Intent;
import android.os.AsyncTask;
import android.util.Log;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
public class MarkReadReceiver extends BroadcastReceiver {
@@ -39,11 +39,10 @@ import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RoutingActivity;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -6,7 +6,7 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.util.Log;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.util.HashSet;
import java.util.LinkedList;

Some files were not shown because too many files have changed in this diff Show More