From 68ec0a3727b848518508f61f4ec51b3fa0942f2c Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 28 Aug 2013 15:35:30 -0700 Subject: [PATCH] Add last resort key and signaling key. --- .../textsecure/crypto/PreKeyUtil.java | 22 +++++++++++++- .../textsecure/push/PreKeyList.java | 10 +++++-- .../textsecure/push/PushServiceSocket.java | 15 +++++++--- .../textsecure/push/SignalingKey.java | 16 ++++++++++ .../textsecure/storage/LocalKeyRecord.java | 4 ++- .../textsecure/storage/RemoteKeyRecord.java | 17 ++++++++++- .../textsecure/util/Medium.java | 2 +- .../RegistrationProgressActivity.java | 17 ++++++----- .../service/RegistrationService.java | 29 ++++++++++++------- .../securesms/util/TextSecurePreferences.java | 8 +++-- 10 files changed, 110 insertions(+), 30 deletions(-) create mode 100644 library/src/org/whispersystems/textsecure/push/SignalingKey.java diff --git a/library/src/org/whispersystems/textsecure/crypto/PreKeyUtil.java b/library/src/org/whispersystems/textsecure/crypto/PreKeyUtil.java index 408ce9a669..c1f3cab284 100644 --- a/library/src/org/whispersystems/textsecure/crypto/PreKeyUtil.java +++ b/library/src/org/whispersystems/textsecure/crypto/PreKeyUtil.java @@ -40,6 +40,24 @@ public class PreKeyUtil { return records; } + public static PreKeyRecord generateLastResortKey(Context context, MasterSecret masterSecret) { + if (PreKeyRecord.hasRecord(context, Medium.MAX_VALUE)) { + try { + return new PreKeyRecord(context, masterSecret, Medium.MAX_VALUE); + } catch (InvalidKeyIdException e) { + Log.w("PreKeyUtil", e); + PreKeyRecord.delete(context, Medium.MAX_VALUE); + } + } + + PreKeyPair keyPair = new PreKeyPair(masterSecret, KeyUtil.generateKeyPair()); + PreKeyRecord record = new PreKeyRecord(context, masterSecret, Medium.MAX_VALUE, keyPair); + + record.save(); + + return record; + } + public static List getPreKeys(Context context, MasterSecret masterSecret) { List records = new LinkedList(); File directory = getPreKeysDirectory(context); @@ -49,7 +67,9 @@ public class PreKeyUtil { for (String keyRecordId : keyRecordIds) { try { - records.add(new PreKeyRecord(context, masterSecret, Integer.parseInt(keyRecordId))); + if (Integer.parseInt(keyRecordId) != Medium.MAX_VALUE) { + records.add(new PreKeyRecord(context, masterSecret, Integer.parseInt(keyRecordId))); + } } catch (InvalidKeyIdException e) { Log.w("PreKeyUtil", e); new File(getPreKeysDirectory(context), keyRecordId).delete(); diff --git a/library/src/org/whispersystems/textsecure/push/PreKeyList.java b/library/src/org/whispersystems/textsecure/push/PreKeyList.java index 5be530b457..b47c32babb 100644 --- a/library/src/org/whispersystems/textsecure/push/PreKeyList.java +++ b/library/src/org/whispersystems/textsecure/push/PreKeyList.java @@ -4,10 +4,12 @@ import java.util.List; public class PreKeyList { + private PreKeyEntity lastResortKey; private List keys; - public PreKeyList(List keys) { - this.keys = keys; + public PreKeyList(PreKeyEntity lastResortKey, List keys) { + this.keys = keys; + this.lastResortKey = lastResortKey; } public List getKeys() { @@ -17,4 +19,8 @@ public class PreKeyList { public static String toJson(PreKeyList entity) { return PreKeyEntity.getBuilder().create().toJson(entity); } + + public PreKeyEntity getLastResortKey() { + return lastResortKey; + } } diff --git a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java index e55ef5c6d5..c363dae0f2 100644 --- a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java +++ b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java @@ -65,8 +65,9 @@ public class PushServiceSocket { makeRequest(String.format(path, localNumber), "POST", null); } - public void verifyAccount(String verificationCode) throws IOException { - makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode), "PUT", null); + public void verifyAccount(String verificationCode, String signalingKey) throws IOException { + SignalingKey signalingKeyEntity = new SignalingKey(signalingKey); + makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode), "PUT", new Gson().toJson(signalingKeyEntity)); } public void registerGcmId(String gcmRegistrationId) throws IOException { @@ -109,7 +110,9 @@ public class PushServiceSocket { throw new IOException("Got send failure: " + response.getFailure().get(0)); } - public void registerPreKeys(IdentityKey identityKey, List records) + public void registerPreKeys(IdentityKey identityKey, + PreKeyRecord lastResortKey, + List records) throws IOException { List entities = new LinkedList(); @@ -121,7 +124,11 @@ public class PushServiceSocket { entities.add(entity); } - makeRequest(String.format(PREKEY_PATH, ""), "PUT", PreKeyList.toJson(new PreKeyList(entities))); + PreKeyEntity lastResortEntity = new PreKeyEntity(lastResortKey.getId(), + lastResortKey.getKeyPair().getPublicKey(), + identityKey); + + makeRequest(String.format(PREKEY_PATH, ""), "PUT", PreKeyList.toJson(new PreKeyList(lastResortEntity, entities))); } public PreKeyEntity getPreKey(String number) throws IOException { diff --git a/library/src/org/whispersystems/textsecure/push/SignalingKey.java b/library/src/org/whispersystems/textsecure/push/SignalingKey.java new file mode 100644 index 0000000000..7d0dcada11 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/push/SignalingKey.java @@ -0,0 +1,16 @@ +package org.whispersystems.textsecure.push; + +public class SignalingKey { + + private String signalingKey; + + public SignalingKey(String signalingKey) { + this.signalingKey = signalingKey; + } + + public SignalingKey() {} + + public String getSignalingKey() { + return signalingKey; + } +} diff --git a/library/src/org/whispersystems/textsecure/storage/LocalKeyRecord.java b/library/src/org/whispersystems/textsecure/storage/LocalKeyRecord.java index 85f2637748..a92f7d0b41 100644 --- a/library/src/org/whispersystems/textsecure/storage/LocalKeyRecord.java +++ b/library/src/org/whispersystems/textsecure/storage/LocalKeyRecord.java @@ -24,6 +24,7 @@ import org.whispersystems.textsecure.crypto.KeyPair; import org.whispersystems.textsecure.crypto.KeyUtil; import org.whispersystems.textsecure.crypto.MasterCipher; import org.whispersystems.textsecure.crypto.MasterSecret; +import org.whispersystems.textsecure.util.Medium; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -65,7 +66,8 @@ public class LocalKeyRecord extends Record { Log.w("LocalKeyRecord", "Remote client acknowledges receiving key id: " + keyId); if (keyId == localNextKeyPair.getId()) { this.localCurrentKeyPair = this.localNextKeyPair; - this.localNextKeyPair = new KeyPair(this.localNextKeyPair.getId()+1, KeyUtil.generateKeyPair(), masterSecret); + this.localNextKeyPair = new KeyPair((this.localNextKeyPair.getId()+1) % Medium.MAX_VALUE, + KeyUtil.generateKeyPair(), masterSecret); } } diff --git a/library/src/org/whispersystems/textsecure/storage/RemoteKeyRecord.java b/library/src/org/whispersystems/textsecure/storage/RemoteKeyRecord.java index 3a3a2c89f7..e3c5606437 100644 --- a/library/src/org/whispersystems/textsecure/storage/RemoteKeyRecord.java +++ b/library/src/org/whispersystems/textsecure/storage/RemoteKeyRecord.java @@ -22,6 +22,7 @@ import android.util.Log; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.PublicKey; import org.whispersystems.textsecure.util.Hex; +import org.whispersystems.textsecure.util.Medium; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -62,7 +63,7 @@ public class RemoteKeyRecord extends Record { public void updateCurrentRemoteKey(PublicKey remoteKey) { Log.w("RemoteKeyRecord", "Updating current remote key: " + remoteKey.getId()); - if (remoteKey.getId() > remoteKeyCurrent.getId()) { + if (isWrappingGreaterThan(remoteKey.getId(), remoteKeyCurrent.getId())) { this.remoteKeyLast = this.remoteKeyCurrent; this.remoteKeyCurrent = remoteKey; } @@ -112,6 +113,20 @@ public class RemoteKeyRecord extends Record { } } + private boolean isWrappingGreaterThan(int receivedValue, int currentValue) { + if (receivedValue > currentValue) { + return true; + } + + if (receivedValue == currentValue) { + return false; + } + + int gap = (receivedValue - currentValue) + Medium.MAX_VALUE; + + return (gap >= 0) && (gap < 5); + } + private void loadData() { Log.w("RemoteKeyRecord", "Loading remote key record for recipient: " + this.address); synchronized (FILE_LOCK) { diff --git a/library/src/org/whispersystems/textsecure/util/Medium.java b/library/src/org/whispersystems/textsecure/util/Medium.java index acaa20086e..30defca019 100644 --- a/library/src/org/whispersystems/textsecure/util/Medium.java +++ b/library/src/org/whispersystems/textsecure/util/Medium.java @@ -1,5 +1,5 @@ package org.whispersystems.textsecure.util; public class Medium { - public static int MAX_VALUE = 0xFFF; + public static int MAX_VALUE = 0xFFFFFF; } diff --git a/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java b/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java index dd086efe3d..0891055aaa 100644 --- a/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java +++ b/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java @@ -432,14 +432,16 @@ public class RegistrationProgressActivity extends SherlockActivity { private final String e164number; private final String password; + private final String signalingKey; private final Context context; private ProgressDialog progressDialog; public VerifyClickListener(String e164number, String password) { - this.e164number = e164number; - this.password = password; - this.context = RegistrationProgressActivity.this; + this.e164number = e164number; + this.password = password; + this.signalingKey = Util.getSecret(52); + this.context = RegistrationProgressActivity.this; } @Override @@ -473,6 +475,7 @@ public class RegistrationProgressActivity extends SherlockActivity { intent.setAction(RegistrationService.VOICE_REGISTER_ACTION); intent.putExtra("e164number", e164number); intent.putExtra("password", password); + intent.putExtra("signaling_key", signalingKey); intent.putExtra("master_secret", masterSecret); startService(intent); break; @@ -495,7 +498,7 @@ public class RegistrationProgressActivity extends SherlockActivity { protected Integer doInBackground(Void... params) { try { PushServiceSocket socket = new PushServiceSocket(context, e164number, password); - socket.verifyAccount(code); + socket.verifyAccount(code, signalingKey); return SUCCESS; } catch (RateLimitException e) { Log.w("RegistrationProgressActivity", e); @@ -521,9 +524,9 @@ public class RegistrationProgressActivity extends SherlockActivity { private final Context context; public CallClickListener(String e164number) { - this.e164number = e164number; - this.password = Util.getSecret(18); - this.context = RegistrationProgressActivity.this; + this.e164number = e164number; + this.password = Util.getSecret(18); + this.context = RegistrationProgressActivity.this; } @Override diff --git a/src/org/thoughtcrime/securesms/service/RegistrationService.java b/src/org/thoughtcrime/securesms/service/RegistrationService.java index 25dd5752f2..02a6db7724 100644 --- a/src/org/thoughtcrime/securesms/service/RegistrationService.java +++ b/src/org/thoughtcrime/securesms/service/RegistrationService.java @@ -153,6 +153,7 @@ public class RegistrationService extends Service { public void run() { if (PreKeyUtil.getPreKeys(RegistrationService.this, masterSecret).size() < PreKeyUtil.BATCH_SIZE) { PreKeyUtil.generatePreKeys(RegistrationService.this, masterSecret); + PreKeyUtil.generateLastResortKey(RegistrationService.this, masterSecret); } synchronized (GENERATING_PREKEYS_SEMAPHOR) { @@ -188,6 +189,7 @@ public class RegistrationService extends Service { String number = intent.getStringExtra("e164number"); String password = intent.getStringExtra("password" ); + String signalingKey = intent.getStringExtra("signaling_key"); MasterSecret masterSecret = intent.getParcelableExtra("master_secret"); try { @@ -198,7 +200,7 @@ public class RegistrationService extends Service { handleCommonRegistration(masterSecret, socket, number); - markAsVerified(number, password); + markAsVerified(number, password, signalingKey); setState(new RegistrationState(RegistrationState.STATE_COMPLETE, number)); broadcastComplete(true); @@ -226,7 +228,9 @@ public class RegistrationService extends Service { MasterSecret masterSecret = intent.getParcelableExtra("master_secret"); try { - String password = Util.getSecret(18); + String password = Util.getSecret(18); + String signalingKey = Util.getSecret(52); + initializeChallengeListener(); initializeGcmRegistrationListener(); initializePreKeyGenerator(masterSecret); @@ -237,10 +241,10 @@ public class RegistrationService extends Service { setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number)); String challenge = waitForChallenge(); - socket.verifyAccount(challenge); + socket.verifyAccount(challenge, signalingKey); handleCommonRegistration(masterSecret, socket, number); - markAsVerified(number, password); + markAsVerified(number, password, signalingKey); setState(new RegistrationState(RegistrationState.STATE_COMPLETE, number)); broadcastComplete(true); @@ -272,7 +276,8 @@ public class RegistrationService extends Service { setState(new RegistrationState(RegistrationState.STATE_GENERATING_KEYS, number)); IdentityKey identityKey = IdentityKeyUtil.getIdentityKey(this); List records = waitForPreKeys(masterSecret); - socket.registerPreKeys(identityKey, records); + PreKeyRecord lastResort = PreKeyUtil.generateLastResortKey(this, masterSecret); + socket.registerPreKeys(identityKey, lastResort, records); setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number)); GCMRegistrar.register(this, GcmIntentService.GCM_SENDER_ID); @@ -280,7 +285,10 @@ public class RegistrationService extends Service { socket.registerGcmId(gcmRegistrationId); Pair directory = socket.retrieveDirectory(); - NumberFilter.getInstance(this).update(directory.first, directory.second); + + if (directory != null) { + NumberFilter.getInstance(this).update(directory.first, directory.second); + } } private synchronized String waitForChallenge() throws AccountVerificationTimeoutException { @@ -347,11 +355,12 @@ public class RegistrationService extends Service { } } - private void markAsVerified(String number, String password) { + private void markAsVerified(String number, String password, String signalingKey) { TextSecurePreferences.setVerifying(this, false); TextSecurePreferences.setPushRegistered(this, true); TextSecurePreferences.setLocalNumber(this, number); TextSecurePreferences.setPushServerPassword(this, password); + TextSecurePreferences.setSignalingKey(this, signalingKey); } private void setState(RegistrationState state) { @@ -432,9 +441,9 @@ public class RegistrationService extends Service { } public RegistrationState(int state, String number, String password) { - this.state = state; - this.number = number; - this.password = password; + this.state = state; + this.number = number; + this.password = password; } } } diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 8aa1fb3ed4..856f658691 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -4,9 +4,6 @@ import android.content.Context; import android.preference.PreferenceManager; import android.util.Log; -import org.thoughtcrime.securesms.ApplicationPreferencesActivity; -import org.thoughtcrime.securesms.RoutingActivity; - public class TextSecurePreferences { public static final String IDENTITY_PREF = "pref_choose_identity"; @@ -40,6 +37,7 @@ public class TextSecurePreferences { private static final String REGISTERED_GCM_PREF = "pref_gcm_registered"; private static final String GCM_PASSWORD_PREF = "pref_gcm_password"; private static final String PROMPTED_PUSH_REGISTRATION_PREF = "pref_prompted_push_registration"; + private static final String SIGNALING_KEY_PREF = "pref_signaling_key"; public static String getLocalNumber(Context context) { return getStringPreference(context, LOCAL_NUMBER_PREF, "No Stored Number"); @@ -57,6 +55,10 @@ public class TextSecurePreferences { setStringPreference(context, GCM_PASSWORD_PREF, password); } + public static void setSignalingKey(Context context, String signalingKey) { + setStringPreference(context, SIGNALING_KEY_PREF, signalingKey); + } + public static boolean isEnterImeKeyEnabled(Context context) { return getBooleanPreference(context, ENTER_PRESENT_PREF, false); }