From bb5dcb7131a9d3a41129b5e6489cc2658ae975ae Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Fri, 6 Jan 2017 09:19:58 -0800 Subject: [PATCH] Start increasing frequency of signed prekey rotation // FREEBIE --- AndroidManifest.xml | 7 +- .../securesms/ApplicationContext.java | 8 ++ .../securesms/ConversationListActivity.java | 1 - .../securesms/crypto/PreKeyUtil.java | 79 +++++++++++++++---- .../crypto/storage/TextSecurePreKeyStore.java | 4 +- .../SignalCommunicationModule.java | 4 +- .../securesms/jobs/CleanPreKeysJob.java | 17 ++-- .../securesms/jobs/CreateSignedPreKeyJob.java | 2 +- .../securesms/jobs/PushGroupSendJob.java | 8 +- .../securesms/jobs/PushMediaSendJob.java | 9 +-- .../securesms/jobs/PushSendJob.java | 18 +++++ .../securesms/jobs/PushTextSendJob.java | 2 +- .../securesms/jobs/RefreshPreKeysJob.java | 3 +- .../securesms/jobs/RotateSignedPreKeyJob.java | 69 ++++++++++++++++ .../service/DirectoryRefreshListener.java | 59 ++++---------- .../PersistentAlarmManagerListener.java | 34 ++++++++ .../service/RegistrationService.java | 3 +- .../service/RotateSignedPreKeyListener.java | 39 +++++++++ .../securesms/util/TextSecurePreferences.java | 18 +++++ 19 files changed, 299 insertions(+), 85 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java create mode 100644 src/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java create mode 100644 src/org/thoughtcrime/securesms/service/RotateSignedPreKeyListener.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ce18289bf6..e7a4f93789 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -502,7 +502,12 @@ - + + + + + + diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 09fbd009fa..87b721196f 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -34,7 +34,9 @@ import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirementProvi import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirementProvider; import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirementProvider; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; +import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.ExpiringMessageManager; +import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.jobqueue.JobManager; import org.whispersystems.jobqueue.dependencies.DependencyInjector; @@ -75,6 +77,7 @@ public class ApplicationContext extends Application implements DependencyInjecto initializeExpiringMessageManager(); initializeGcmCheck(); initializeSignedPreKeyCheck(); + initializePeriodicTasks(); } @Override @@ -150,4 +153,9 @@ public class ApplicationContext extends Application implements DependencyInjecto this.expiringMessageManager = new ExpiringMessageManager(this); } + private void initializePeriodicTasks() { + RotateSignedPreKeyListener.schedule(this); + DirectoryRefreshListener.schedule(this); + } + } diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index 7ec9d40409..ce3ec32514 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -73,7 +73,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit initializeContactUpdatesReceiver(); - DirectoryRefreshListener.schedule(this); RatingManager.showRatingDialogIfNecessary(this); } diff --git a/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java b/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java index 7333bfbfa9..84981a90f9 100644 --- a/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java +++ b/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java @@ -35,6 +35,7 @@ import org.whispersystems.libsignal.state.PreKeyStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.libsignal.util.Medium; +import org.whispersystems.libsignal.util.guava.Optional; import java.io.File; import java.io.FileInputStream; @@ -46,7 +47,9 @@ import java.util.List; public class PreKeyUtil { - public static final int BATCH_SIZE = 100; + private static final String TAG = PreKeyUtil.class.getName(); + + private static final int BATCH_SIZE = 100; public static List generatePreKeys(Context context) { PreKeyStore preKeyStore = new TextSecurePreKeyStore(context); @@ -66,7 +69,7 @@ public class PreKeyUtil { return records; } - public static SignedPreKeyRecord generateSignedPreKey(Context context, IdentityKeyPair identityKeyPair) + public static SignedPreKeyRecord generateSignedPreKey(Context context, IdentityKeyPair identityKeyPair, boolean active) { try { SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context); @@ -78,6 +81,10 @@ public class PreKeyUtil { signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record); setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE); + if (active) { + setActiveSignedPreKeyId(context, signedPreKeyId); + } + return record; } catch (InvalidKeyException e) { throw new AssertionError(e); @@ -104,7 +111,7 @@ public class PreKeyUtil { return record; } - private static void setNextPreKeyId(Context context, int id) { + private static synchronized void setNextPreKeyId(Context context, int id) { try { File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME); FileOutputStream fout = new FileOutputStream(nextFile); @@ -115,18 +122,36 @@ public class PreKeyUtil { } } - private static void setNextSignedPreKeyId(Context context, int id) { + private static synchronized void setNextSignedPreKeyId(Context context, int id) { try { - File nextFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME); - FileOutputStream fout = new FileOutputStream(nextFile); - fout.write(JsonUtils.toJson(new SignedPreKeyIndex(id)).getBytes()); - fout.close(); + SignedPreKeyIndex index = getSignedPreKeyIndex(context).or(new SignedPreKeyIndex()); + index.nextSignedPreKeyId = id; + + setSignedPreKeyIndex(context, index); } catch (IOException e) { - Log.w("PreKeyUtil", e); + Log.w(TAG, e); } } - private static int getNextPreKeyId(Context context) { + public static synchronized void setActiveSignedPreKeyId(Context context, int id) { + try { + SignedPreKeyIndex index = getSignedPreKeyIndex(context).or(new SignedPreKeyIndex()); + index.activeSignedPreKeyId = id; + + setSignedPreKeyIndex(context, index); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + public static synchronized int getActiveSignedPreKeyId(Context context) { + Optional index = getSignedPreKeyIndex(context); + + if (index.isPresent()) return index.get().activeSignedPreKeyId; + else return -1; + } + + private static synchronized int getNextPreKeyId(Context context) { try { File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME); @@ -144,7 +169,7 @@ public class PreKeyUtil { } } - private static int getNextSignedPreKeyId(Context context) { + private static synchronized int getNextSignedPreKeyId(Context context) { try { File nextFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME); @@ -162,6 +187,32 @@ public class PreKeyUtil { } } + private static synchronized Optional getSignedPreKeyIndex(Context context) { + File indexFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME); + + if (!indexFile.exists()) { + return Optional.absent(); + } + + try { + InputStreamReader reader = new InputStreamReader(new FileInputStream(indexFile)); + SignedPreKeyIndex index = JsonUtils.fromJson(reader, SignedPreKeyIndex.class); + reader.close(); + + return Optional.of(index); + } catch (IOException e) { + Log.w(TAG, e); + return Optional.absent(); + } + } + + private static synchronized void setSignedPreKeyIndex(Context context, SignedPreKeyIndex index) throws IOException { + File indexFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME); + FileOutputStream fout = new FileOutputStream(indexFile); + fout.write(JsonUtils.toJson(index).getBytes()); + fout.close(); + } + private static File getPreKeysDirectory(Context context) { return getKeysDirectory(context, TextSecurePreKeyStore.PREKEY_DIRECTORY); } @@ -198,11 +249,11 @@ public class PreKeyUtil { @JsonProperty private int nextSignedPreKeyId; + @JsonProperty + private int activeSignedPreKeyId = -1; + public SignedPreKeyIndex() {} - public SignedPreKeyIndex(int nextSignedPreKeyId) { - this.nextSignedPreKeyId = nextSignedPreKeyId; - } } diff --git a/src/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java b/src/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java index 68c727caa5..3e27dd6019 100644 --- a/src/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java +++ b/src/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java @@ -79,7 +79,9 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore { for (File signedPreKeyFile : directory.listFiles()) { try { - results.add(new SignedPreKeyRecord(loadSerializedRecord(signedPreKeyFile))); + if (!"index.dat".equals(signedPreKeyFile.getName())) { + results.add(new SignedPreKeyRecord(loadSerializedRecord(signedPreKeyFile))); + } } catch (IOException | InvalidMessageException e) { Log.w(TAG, e); } diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index 3002d78b8f..bce24e0c29 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.jobs.PushTextSendJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob; +import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob; import org.thoughtcrime.securesms.push.SecurityEventListener; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.service.MessageRetrievalService; @@ -56,7 +57,8 @@ import dagger.Provides; GcmRefreshJob.class, RequestGroupInfoJob.class, PushGroupUpdateJob.class, - AvatarDownloadJob.class}) + AvatarDownloadJob.class, + RotateSignedPreKeyJob.class}) public class SignalCommunicationModule { private final Context context; diff --git a/src/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java b/src/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java index 22d597ccd1..8641b8a074 100644 --- a/src/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java +++ b/src/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java @@ -4,6 +4,7 @@ import android.content.Context; import android.util.Log; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.whispersystems.jobqueue.JobParameters; @@ -11,7 +12,6 @@ import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; @@ -30,7 +30,7 @@ public class CleanPreKeysJob extends MasterSecretJob implements InjectableType { private static final String TAG = CleanPreKeysJob.class.getSimpleName(); - private static final int ARCHIVE_AGE_DAYS = 15; + private static final long ARCHIVE_AGE = TimeUnit.DAYS.toMillis(7); @Inject transient SignalServiceAccountManager accountManager; @Inject transient SignedPreKeyStoreFactory signedPreKeyStoreFactory; @@ -51,17 +51,20 @@ public class CleanPreKeysJob extends MasterSecretJob implements InjectableType { @Override public void onRun(MasterSecret masterSecret) throws IOException { try { - SignedPreKeyStore signedPreKeyStore = signedPreKeyStoreFactory.create(); - SignedPreKeyEntity currentSignedPreKey = accountManager.getSignedPreKey(); + Log.w(TAG, "Cleaning prekeys..."); - if (currentSignedPreKey == null) return; + int activeSignedPreKeyId = PreKeyUtil.getActiveSignedPreKeyId(context); + SignedPreKeyStore signedPreKeyStore = signedPreKeyStoreFactory.create(); - SignedPreKeyRecord currentRecord = signedPreKeyStore.loadSignedPreKey(currentSignedPreKey.getKeyId()); + if (activeSignedPreKeyId < 0) return; + + SignedPreKeyRecord currentRecord = signedPreKeyStore.loadSignedPreKey(activeSignedPreKeyId); List allRecords = signedPreKeyStore.loadSignedPreKeys(); LinkedList oldRecords = removeRecordFrom(currentRecord, allRecords); Collections.sort(oldRecords, new SignedPreKeySorter()); + Log.w(TAG, "Active signed prekey: " + activeSignedPreKeyId); Log.w(TAG, "Old signed prekey record count: " + oldRecords.size()); boolean foundAgedRecord = false; @@ -69,7 +72,7 @@ public class CleanPreKeysJob extends MasterSecretJob implements InjectableType { for (SignedPreKeyRecord oldRecord : oldRecords) { long archiveDuration = System.currentTimeMillis() - oldRecord.getTimestamp(); - if (archiveDuration >= TimeUnit.DAYS.toMillis(ARCHIVE_AGE_DAYS)) { + if (archiveDuration >= ARCHIVE_AGE) { if (!foundAgedRecord) { foundAgedRecord = true; } else { diff --git a/src/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java b/src/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java index 6bc0aeee72..78639cb22c 100644 --- a/src/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java +++ b/src/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java @@ -53,7 +53,7 @@ public class CreateSignedPreKeyJob extends MasterSecretJob implements Injectable } IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context); - SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKeyPair); + SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKeyPair, true); accountManager.setSignedPreKey(signedPreKeyRecord); TextSecurePreferences.setSignedPreKeyRegistered(context, true); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 4eaf156fbf..b4fe814013 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -70,12 +70,10 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { @Override public void onAdded() { -// DatabaseFactory.getMmsDatabase(context) -// .markAsSending(messageId); } @Override - public void onSend(MasterSecret masterSecret) + public void onPushSend(MasterSecret masterSecret) throws MmsException, IOException, NoSuchMessageException { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); @@ -84,8 +82,6 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { try { deliver(masterSecret, message, filterRecipientId); -// database.markAsPush(messageId); -// database.markAsSecure(messageId); database.markAsSent(messageId, true); markAttachmentsUploaded(messageId, message.getAttachments()); @@ -114,10 +110,8 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { } database.addFailures(messageId, failures); -// database.markAsPush(messageId); if (e.getNetworkExceptions().isEmpty() && e.getUntrustedIdentityExceptions().isEmpty()) { -// database.markAsSecure(messageId); database.markAsSent(messageId, true); markAttachmentsUploaded(messageId, message.getAttachments()); } else { diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index d2bdf7c68d..4964547827 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -53,13 +53,11 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { @Override public void onAdded() { -// MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); -// mmsDatabase.markAsSending(messageId); -// mmsDatabase.markAsPush(messageId); + } @Override - public void onSend(MasterSecret masterSecret) + public void onPushSend(MasterSecret masterSecret) throws RetryLaterException, MmsException, NoSuchMessageException, UndeliverableMessageException { @@ -69,8 +67,6 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { try { deliver(masterSecret, message); -// database.markAsPush(messageId); -// database.markAsSecure(messageId); database.markAsSent(messageId, true); markAttachmentsUploaded(messageId, message.getAttachments()); @@ -91,7 +87,6 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { database.addMismatchedIdentity(messageId, recipientId, uie.getIdentityKey()); database.markAsSentFailed(messageId); -// database.markAsPush(messageId); } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java index ca23d0046e..679c8c8b9d 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.jobs; import android.content.Context; import android.util.Log; +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.TextSecureExpiredException; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -12,6 +14,7 @@ import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.requirements.NetworkRequirement; @@ -48,6 +51,19 @@ public abstract class PushSendJob extends SendJob { return builder.create(); } + @Override + protected final void onSend(MasterSecret masterSecret) throws Exception { + if (TextSecurePreferences.getSignedPreKeyFailureCount(context) > 5) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new RotateSignedPreKeyJob(context)); + + throw new TextSecureExpiredException("Too many signed prekey rotation failures"); + } + + onPushSend(masterSecret); + } + protected SignalServiceAddress getPushAddress(String number) throws InvalidNumberException { String e164number = Util.canonicalizeNumber(context, number); String relay = TextSecureDirectory.getInstance(context).getRelay(e164number); @@ -93,4 +109,6 @@ public abstract class PushSendJob extends SendJob { MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId); } } + + protected abstract void onPushSend(MasterSecret masterSecret) throws Exception; } diff --git a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index a875698a04..52bce00ddf 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -49,7 +49,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { public void onAdded() {} @Override - public void onSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException { + public void onPushSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException { ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager(); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); SmsMessageRecord record = database.getMessage(masterSecret, messageId); diff --git a/src/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java b/src/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java index 8c088d3c4a..af72ddb7dc 100644 --- a/src/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java @@ -60,12 +60,13 @@ public class RefreshPreKeysJob extends MasterSecretJob implements InjectableType List preKeyRecords = PreKeyUtil.generatePreKeys(context); PreKeyRecord lastResortKeyRecord = PreKeyUtil.generateLastResortKey(context); IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context); - SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKey); + SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKey, false); Log.w(TAG, "Registering new prekeys..."); accountManager.setPreKeys(identityKey.getPublicKey(), lastResortKeyRecord, signedPreKeyRecord, preKeyRecords); + PreKeyUtil.setActiveSignedPreKeyId(context, signedPreKeyRecord.getId()); TextSecurePreferences.setSignedPreKeyRegistered(context, true); ApplicationContext.getInstance(context) diff --git a/src/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java b/src/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java new file mode 100644 index 0000000000..050bc0240c --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java @@ -0,0 +1,69 @@ +package org.thoughtcrime.securesms.jobs; + + +import android.content.Context; +import android.util.Log; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.crypto.PreKeyUtil; +import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.jobqueue.JobParameters; +import org.whispersystems.jobqueue.requirements.NetworkRequirement; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; + +import javax.inject.Inject; + +public class RotateSignedPreKeyJob extends MasterSecretJob implements InjectableType { + + private static final String TAG = RotateSignedPreKeyJob.class.getName(); + + @Inject transient SignalServiceAccountManager accountManager; + + public RotateSignedPreKeyJob(Context context) { + super(context, JobParameters.newBuilder() + .withRequirement(new NetworkRequirement(context)) + .withRequirement(new MasterSecretRequirement(context)) + .withRetryCount(5) + .create()); + } + + @Override + public void onAdded() { + + } + + @Override + public void onRun(MasterSecret masterSecret) throws Exception { + Log.w(TAG, "Rotating signed prekey..."); + + IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context); + SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKey, false); + + accountManager.setSignedPreKey(signedPreKeyRecord); + + PreKeyUtil.setActiveSignedPreKeyId(context, signedPreKeyRecord.getId()); + TextSecurePreferences.setSignedPreKeyRegistered(context, true); + TextSecurePreferences.setSignedPreKeyFailureCount(context, 0); + + ApplicationContext.getInstance(context) + .getJobManager() + .add(new CleanPreKeysJob(context)); + } + + @Override + public boolean onShouldRetryThrowable(Exception exception) { + return exception instanceof PushNetworkException; + } + + @Override + public void onCanceled() { + TextSecurePreferences.setSignedPreKeyFailureCount(context, TextSecurePreferences.getSignedPreKeyFailureCount(context) + 1); + } +} diff --git a/src/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java b/src/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java index 9e70ce8cd9..53300b07aa 100644 --- a/src/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java +++ b/src/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java @@ -1,64 +1,39 @@ package org.thoughtcrime.securesms.service; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.util.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; import org.thoughtcrime.securesms.util.TextSecurePreferences; -public class DirectoryRefreshListener extends BroadcastReceiver { +import java.util.concurrent.TimeUnit; - private static final String TAG = DirectoryRefreshListener.class.getSimpleName(); +public class DirectoryRefreshListener extends PersistentAlarmManagerListener { - private static final String REFRESH_EVENT = "org.whispersystems.whisperpush.DIRECTORY_REFRESH"; - private static final String BOOT_EVENT = "android.intent.action.BOOT_COMPLETED"; - - private static final long INTERVAL = 12 * 60 * 60 * 1000; // 12 hours. + private static final long INTERVAL = TimeUnit.HOURS.toMillis(12); @Override - public void onReceive(Context context, Intent intent) { - if (REFRESH_EVENT.equals(intent.getAction())) handleRefreshAction(context); - else if (BOOT_EVENT.equals(intent.getAction())) handleBootEvent(context); + protected long getNextScheduledExecutionTime(Context context) { + return TextSecurePreferences.getDirectoryRefreshTime(context); } - private void handleBootEvent(Context context) { - schedule(context); - } + @Override + protected long onAlarm(Context context, long scheduledTime) { + if (scheduledTime != 0 && TextSecurePreferences.isPushRegistered(context)) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new DirectoryRefreshJob(context)); + } - private void handleRefreshAction(Context context) { - schedule(context); + long newTime = System.currentTimeMillis() + INTERVAL; + TextSecurePreferences.setDirectoryRefreshTime(context, newTime); + + return newTime; } public static void schedule(Context context) { - if (!TextSecurePreferences.isPushRegistered(context)) return; - - AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(DirectoryRefreshListener.REFRESH_EVENT); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0); - long time = TextSecurePreferences.getDirectoryRefreshTime(context); - - if (time <= System.currentTimeMillis()) { - if (time != 0) { - ApplicationContext.getInstance(context) - .getJobManager() - .add(new DirectoryRefreshJob(context)); - } - - time = System.currentTimeMillis() + INTERVAL; - } - - Log.w(TAG, "Scheduling for: " + time); - - alarmManager.cancel(pendingIntent); - alarmManager.set(AlarmManager.RTC_WAKEUP, time, pendingIntent); - - TextSecurePreferences.setDirectoryRefreshTime(context, time); + new DirectoryRefreshListener().onReceive(context, new Intent()); } - } diff --git a/src/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java b/src/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java new file mode 100644 index 0000000000..f592d9f988 --- /dev/null +++ b/src/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.service; + + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public abstract class PersistentAlarmManagerListener extends BroadcastReceiver { + + private static final String TAG = PersistentAlarmManagerListener.class.getSimpleName(); + + protected abstract long getNextScheduledExecutionTime(Context context); + protected abstract long onAlarm(Context context, long scheduledTime); + + @Override + public void onReceive(Context context, Intent intent) { + long scheduledTime = getNextScheduledExecutionTime(context); + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent alarmIntent = new Intent(context, getClass()); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0); + + if (System.currentTimeMillis() >= scheduledTime) { + scheduledTime = onAlarm(context, scheduledTime); + } + + Log.w(TAG, getClass() + " scheduling for: " + scheduledTime); + + alarmManager.cancel(pendingIntent); + alarmManager.set(AlarmManager.RTC_WAKEUP, scheduledTime, pendingIntent); + } +} diff --git a/src/org/thoughtcrime/securesms/service/RegistrationService.java b/src/org/thoughtcrime/securesms/service/RegistrationService.java index bd5f282817..ec9f4c1514 100644 --- a/src/org/thoughtcrime/securesms/service/RegistrationService.java +++ b/src/org/thoughtcrime/securesms/service/RegistrationService.java @@ -239,7 +239,7 @@ public class RegistrationService extends Service { IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(this); List records = PreKeyUtil.generatePreKeys(this); PreKeyRecord lastResort = PreKeyUtil.generateLastResortKey(this); - SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(this, identityKey); + SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(this, identityKey, true); accountManager.setPreKeys(identityKey.getPublicKey(),lastResort, signedPreKey, records); setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number)); @@ -261,6 +261,7 @@ public class RegistrationService extends Service { redPhoneAccountManager.createAccount(verificationToken, new RedPhoneAccountAttributes(signalingKey, gcmRegistrationId)); DirectoryRefreshListener.schedule(this); + RotateSignedPreKeyListener.schedule(this); } private synchronized String waitForChallenge() throws AccountVerificationTimeoutException { diff --git a/src/org/thoughtcrime/securesms/service/RotateSignedPreKeyListener.java b/src/org/thoughtcrime/securesms/service/RotateSignedPreKeyListener.java new file mode 100644 index 0000000000..0eee795d62 --- /dev/null +++ b/src/org/thoughtcrime/securesms/service/RotateSignedPreKeyListener.java @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.service; + + +import android.content.Context; +import android.content.Intent; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob; +import org.thoughtcrime.securesms.util.TextSecurePreferences; + +import java.util.concurrent.TimeUnit; + +public class RotateSignedPreKeyListener extends PersistentAlarmManagerListener { + + private static final long INTERVAL = TimeUnit.DAYS.toMillis(2); + + @Override + protected long getNextScheduledExecutionTime(Context context) { + return TextSecurePreferences.getSignedPreKeyRotationTime(context); + } + + @Override + protected long onAlarm(Context context, long scheduledTime) { + if (scheduledTime != 0 && TextSecurePreferences.isPushRegistered(context)) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new RotateSignedPreKeyJob(context)); + } + + long nextTime = System.currentTimeMillis() + INTERVAL; + TextSecurePreferences.setSignedPreKeyRotationTime(context, nextTime); + + return nextTime; + } + + public static void schedule(Context context) { + new RotateSignedPreKeyListener().onReceive(context, new Intent()); + } +} diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 2d985a2d35..0a4e27adc3 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -72,6 +72,7 @@ public class TextSecurePreferences { private static final String PROMPTED_SHARE_PREF = "pref_prompted_share"; private static final String SIGNALING_KEY_PREF = "pref_signaling_key"; private static final String DIRECTORY_FRESH_TIME_PREF = "pref_directory_refresh_time"; + private static final String SIGNED_PREKEY_ROTATION_TIME_PREF = "pref_signed_pre_key_rotation_time"; private static final String IN_THREAD_NOTIFICATION_PREF = "pref_key_inthread_notifications"; private static final String BLOCKING_IDENTITY_CHANGES_PREF = "pref_blocking_identity_changes"; @@ -84,6 +85,7 @@ public class TextSecurePreferences { private static final String WEBSOCKET_REGISTERED_PREF = "pref_websocket_registered"; private static final String RATING_LATER_PREF = "pref_rating_later"; private static final String RATING_ENABLED_PREF = "pref_rating_enabled"; + private static final String SIGNED_PREKEY_FAILURE_COUNT_PREF = "pref_signed_prekey_failure_count"; public static final String REPEAT_ALERTS_PREF = "pref_repeat_alerts"; public static final String NOTIFICATION_PRIVACY_PREF = "pref_notification_privacy"; @@ -122,6 +124,14 @@ public class TextSecurePreferences { setBooleanPreference(context, BLOCKING_IDENTITY_CHANGES_PREF, value); } + public static void setSignedPreKeyFailureCount(Context context, int value) { + setIntegerPrefrence(context, SIGNED_PREKEY_FAILURE_COUNT_PREF, value); + } + + public static int getSignedPreKeyFailureCount(Context context) { + return getIntegerPreference(context, SIGNED_PREKEY_FAILURE_COUNT_PREF, 0); + } + public static NotificationPrivacyPreference getNotificationPrivacy(Context context) { return new NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all")); } @@ -214,6 +224,14 @@ public class TextSecurePreferences { return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true); } + public static long getSignedPreKeyRotationTime(Context context) { + return getLongPreference(context, SIGNED_PREKEY_ROTATION_TIME_PREF, 0L); + } + + public static void setSignedPreKeyRotationTime(Context context, long value) { + setLongPreference(context, SIGNED_PREKEY_ROTATION_TIME_PREF, value); + } + public static long getDirectoryRefreshTime(Context context) { return getLongPreference(context, DIRECTORY_FRESH_TIME_PREF, 0L); }