diff --git a/library/src/org/whispersystems/textsecure/push/PreKeyList.java b/library/src/org/whispersystems/textsecure/push/PreKeyList.java new file mode 100644 index 0000000000..5c1bac15b9 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/push/PreKeyList.java @@ -0,0 +1,16 @@ +package org.whispersystems.textsecure.push; + +import java.util.List; + +public class PreKeyList { + + private List keys; + + public PreKeyList(List keys) { + this.keys = keys; + } + + public List getKeys() { + return keys; + } +} diff --git a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java index 45a048d705..9ef057c834 100644 --- a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java +++ b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java @@ -39,6 +39,7 @@ public class PushServiceSocket { private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/%s"; private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s"; private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/"; + private static final String PREKEY_PATH = "/v1/keys/"; private static final String DIRECTORY_PATH = "/v1/directory/"; private static final String MESSAGE_PATH = "/v1/messages/"; @@ -105,6 +106,11 @@ public class PushServiceSocket { throw new IOException("Got send failure: " + response.getFailure().get(0)); } + public void registerPreKeys(PreKeyList keys) throws IOException { + makeRequest(PREKEY_PATH, "PUT", new Gson().toJson(keys)); + } + + private List sendAttachments(List attachments) throws IOException { diff --git a/libs/protobuf-java-2.4.1.jar b/libs/protobuf-java-2.4.1.jar new file mode 100644 index 0000000000..1373fa424d Binary files /dev/null and b/libs/protobuf-java-2.4.1.jar differ diff --git a/protobuf/Makefile b/protobuf/Makefile new file mode 100644 index 0000000000..836c360af6 --- /dev/null +++ b/protobuf/Makefile @@ -0,0 +1,3 @@ + +all: + protoc --java_out=../src/ PreKeyEntity.proto diff --git a/protobuf/PreKeyEntity.proto b/protobuf/PreKeyEntity.proto new file mode 100644 index 0000000000..bce42a3f24 --- /dev/null +++ b/protobuf/PreKeyEntity.proto @@ -0,0 +1,9 @@ +package textsecure; + +option java_package = "org.thoughtcrime.securesms.encoded"; +option java_outer_classname = "PreKeyProtos"; + +message PreKeyEntity { + optional uint64 id = 1; + optional bytes key = 2; +} \ No newline at end of file diff --git a/res/layout/registration_progress_activity.xml b/res/layout/registration_progress_activity.xml index 7c43613e00..7425dcc823 100644 --- a/res/layout/registration_progress_activity.xml +++ b/res/layout/registration_progress_activity.xml @@ -388,6 +388,50 @@ android:textSize="16.0sp" /> + + + + + + + + + + + + SMS verification failed. + Generating keys... To diff --git a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index 93d3d5b37e..443d07837a 100644 --- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -369,7 +369,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr } }.execute(); } else { - startActivity(new Intent(ApplicationPreferencesActivity.this, RegistrationActivity.class)); + Intent intent = new Intent(ApplicationPreferencesActivity.this, RegistrationActivity.class); + intent.putExtra("master_secret", getIntent().getParcelableExtra("master_secret")); + startActivity(intent); } return false; diff --git a/src/org/thoughtcrime/securesms/PassphraseCreateActivity.java b/src/org/thoughtcrime/securesms/PassphraseCreateActivity.java index c403e8c59c..b467c74a3e 100644 --- a/src/org/thoughtcrime/securesms/PassphraseCreateActivity.java +++ b/src/org/thoughtcrime/securesms/PassphraseCreateActivity.java @@ -27,6 +27,7 @@ import android.widget.Toast; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.VersionTracker; import org.whispersystems.textsecure.util.Util; diff --git a/src/org/thoughtcrime/securesms/RegistrationActivity.java b/src/org/thoughtcrime/securesms/RegistrationActivity.java index 7ca3e08ebf..af061ef722 100644 --- a/src/org/thoughtcrime/securesms/RegistrationActivity.java +++ b/src/org/thoughtcrime/securesms/RegistrationActivity.java @@ -24,6 +24,7 @@ import com.google.i18n.phonenumbers.AsYouTypeFormatter; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; +import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.util.PhoneNumberFormatter; import org.whispersystems.textsecure.util.Util; @@ -47,6 +48,8 @@ public class RegistrationActivity extends SherlockActivity { private Button createButton; private Button skipButton; + private MasterSecret masterSecret; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -70,6 +73,7 @@ public class RegistrationActivity extends SherlockActivity { } private void initializeResources() { + this.masterSecret = getIntent().getParcelableExtra("master_secret"); this.countrySpinner = (Spinner)findViewById(R.id.country_spinner); this.countryCode = (TextView)findViewById(R.id.country_code); this.number = (TextView)findViewById(R.id.number); @@ -191,6 +195,7 @@ public class RegistrationActivity extends SherlockActivity { public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(self, RegistrationProgressActivity.class); intent.putExtra("e164number", e164number); + intent.putExtra("master_secret", masterSecret); startActivity(intent); finish(); } diff --git a/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java b/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java index 722af7c2b2..9ed2da33d2 100644 --- a/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java +++ b/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java @@ -29,9 +29,10 @@ import android.widget.TextView; import android.widget.Toast; import com.actionbarsherlock.app.SherlockActivity; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.service.RegistrationService; import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.RateLimitException; -import org.thoughtcrime.securesms.service.RegistrationService; import org.whispersystems.textsecure.util.PhoneNumberFormatter; import org.whispersystems.textsecure.util.Util; @@ -58,15 +59,19 @@ public class RegistrationProgressActivity extends SherlockActivity { private ProgressBar registrationProgress; private ProgressBar connectingProgress; private ProgressBar verificationProgress; + private ProgressBar generatingKeysProgress; private ProgressBar gcmRegistrationProgress; + private ImageView connectingCheck; private ImageView verificationCheck; + private ImageView generatingKeysCheck; private ImageView gcmRegistrationCheck; private TextView connectingText; private TextView verificationText; private TextView registrationTimerText; + private TextView generatingKeysText; private TextView gcmRegistrationText; private Button verificationFailureButton; @@ -76,6 +81,7 @@ public class RegistrationProgressActivity extends SherlockActivity { private EditText codeEditText; + private MasterSecret masterSecret; private volatile boolean visible; @Override @@ -118,19 +124,23 @@ public class RegistrationProgressActivity extends SherlockActivity { } private void initializeResources() { + this.masterSecret = getIntent().getParcelableExtra("master_secret"); this.registrationLayout = (LinearLayout)findViewById(R.id.registering_layout); this.verificationFailureLayout = (LinearLayout)findViewById(R.id.verification_failure_layout); this.connectivityFailureLayout = (LinearLayout)findViewById(R.id.connectivity_failure_layout); this.registrationProgress = (ProgressBar) findViewById(R.id.registration_progress); this.connectingProgress = (ProgressBar) findViewById(R.id.connecting_progress); this.verificationProgress = (ProgressBar) findViewById(R.id.verification_progress); + this.generatingKeysProgress = (ProgressBar) findViewById(R.id.generating_keys_progress); this.gcmRegistrationProgress = (ProgressBar) findViewById(R.id.gcm_registering_progress); this.connectingCheck = (ImageView) findViewById(R.id.connecting_complete); this.verificationCheck = (ImageView) findViewById(R.id.verification_complete); + this.generatingKeysCheck = (ImageView) findViewById(R.id.generating_keys_complete); this.gcmRegistrationCheck = (ImageView) findViewById(R.id.gcm_registering_complete); this.connectingText = (TextView) findViewById(R.id.connecting_text); this.verificationText = (TextView) findViewById(R.id.verification_text); this.registrationTimerText = (TextView) findViewById(R.id.registration_timer); + this.generatingKeysText = (TextView) findViewById(R.id.generating_keys_text); this.gcmRegistrationText = (TextView) findViewById(R.id.gcm_registering_text); this.verificationFailureButton = (Button) findViewById(R.id.verification_failure_edit_button); this.connectivityFailureButton = (Button) findViewById(R.id.connectivity_failure_edit_button); @@ -181,6 +191,7 @@ public class RegistrationProgressActivity extends SherlockActivity { Intent intent = new Intent(this, RegistrationService.class); intent.setAction(RegistrationService.REGISTER_NUMBER_ACTION); intent.putExtra("e164number", getNumberDirective()); + intent.putExtra("master_secret", masterSecret); startService(intent); } else { startActivity(new Intent(this, RegistrationActivity.class)); @@ -196,10 +207,13 @@ public class RegistrationProgressActivity extends SherlockActivity { this.connectingCheck.setVisibility(View.INVISIBLE); this.verificationProgress.setVisibility(View.INVISIBLE); this.verificationCheck.setVisibility(View.INVISIBLE); + this.generatingKeysProgress.setVisibility(View.INVISIBLE); + this.generatingKeysCheck.setVisibility(View.INVISIBLE); this.gcmRegistrationProgress.setVisibility(View.INVISIBLE); this.gcmRegistrationCheck.setVisibility(View.INVISIBLE); this.connectingText.setTextColor(FOCUSED_COLOR); this.verificationText.setTextColor(UNFOCUSED_COLOR); + this.generatingKeysText.setTextColor(UNFOCUSED_COLOR); this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR); this.timeoutProgressLayout.setVisibility(View.VISIBLE); } @@ -212,15 +226,38 @@ public class RegistrationProgressActivity extends SherlockActivity { this.connectingCheck.setVisibility(View.VISIBLE); this.verificationProgress.setVisibility(View.VISIBLE); this.verificationCheck.setVisibility(View.INVISIBLE); + this.generatingKeysProgress.setVisibility(View.INVISIBLE); + this.generatingKeysCheck.setVisibility(View.INVISIBLE); this.gcmRegistrationProgress.setVisibility(View.INVISIBLE); this.gcmRegistrationCheck.setVisibility(View.INVISIBLE); this.connectingText.setTextColor(UNFOCUSED_COLOR); this.verificationText.setTextColor(FOCUSED_COLOR); + this.generatingKeysText.setTextColor(UNFOCUSED_COLOR); this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR); this.registrationProgress.setVisibility(View.VISIBLE); this.timeoutProgressLayout.setVisibility(View.VISIBLE); } + private void handleStateGeneratingKeys() { + this.registrationLayout.setVisibility(View.VISIBLE); + this.verificationFailureLayout.setVisibility(View.GONE); + this.connectivityFailureLayout.setVisibility(View.GONE); + this.connectingProgress.setVisibility(View.INVISIBLE); + this.connectingCheck.setVisibility(View.VISIBLE); + this.verificationProgress.setVisibility(View.INVISIBLE); + this.verificationCheck.setVisibility(View.VISIBLE); + this.generatingKeysProgress.setVisibility(View.VISIBLE); + this.generatingKeysCheck.setVisibility(View.INVISIBLE); + this.gcmRegistrationProgress.setVisibility(View.INVISIBLE); + this.gcmRegistrationCheck.setVisibility(View.INVISIBLE); + this.connectingText.setTextColor(UNFOCUSED_COLOR); + this.verificationText.setTextColor(UNFOCUSED_COLOR); + this.generatingKeysText.setTextColor(FOCUSED_COLOR); + this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR); + this.registrationProgress.setVisibility(View.INVISIBLE); + this.timeoutProgressLayout.setVisibility(View.INVISIBLE); + } + private void handleStateGcmRegistering() { this.registrationLayout.setVisibility(View.VISIBLE); this.verificationFailureLayout.setVisibility(View.GONE); @@ -229,10 +266,13 @@ public class RegistrationProgressActivity extends SherlockActivity { this.connectingCheck.setVisibility(View.VISIBLE); this.verificationProgress.setVisibility(View.INVISIBLE); this.verificationCheck.setVisibility(View.VISIBLE); + this.generatingKeysProgress.setVisibility(View.INVISIBLE); + this.generatingKeysCheck.setVisibility(View.VISIBLE); this.gcmRegistrationProgress.setVisibility(View.VISIBLE); this.gcmRegistrationCheck.setVisibility(View.INVISIBLE); this.connectingText.setTextColor(UNFOCUSED_COLOR); this.verificationText.setTextColor(UNFOCUSED_COLOR); + this.generatingKeysText.setTextColor(UNFOCUSED_COLOR); this.gcmRegistrationText.setTextColor(FOCUSED_COLOR); this.registrationProgress.setVisibility(View.INVISIBLE); this.timeoutProgressLayout.setVisibility(View.INVISIBLE); @@ -351,6 +391,7 @@ public class RegistrationProgressActivity extends SherlockActivity { case RegistrationState.STATE_CONNECTING: handleStateConnecting(); break; case RegistrationState.STATE_VERIFYING: handleStateVerifying(); break; case RegistrationState.STATE_TIMER: handleTimerUpdate(); break; + case RegistrationState.STATE_GENERATING_KEYS: handleStateGeneratingKeys(); break; case RegistrationState.STATE_GCM_REGISTERING: handleStateGcmRegistering(); break; case RegistrationState.STATE_TIMEOUT: handleVerificationTimeout(state); break; case RegistrationState.STATE_COMPLETE: handleVerificationComplete(); break; @@ -429,6 +470,7 @@ public class RegistrationProgressActivity extends SherlockActivity { intent.setAction(RegistrationService.VOICE_REGISTER_ACTION); intent.putExtra("e164number", e164number); intent.putExtra("password", password); + intent.putExtra("master_secret", masterSecret); startService(intent); break; case NETWORK_ERROR: @@ -504,6 +546,7 @@ public class RegistrationProgressActivity extends SherlockActivity { intent.setAction(RegistrationService.VOICE_REQUESTED_ACTION); intent.putExtra("e164number", e164number); intent.putExtra("password", password); + intent.putExtra("master_secret", masterSecret); startService(intent); callButton.setEnabled(false); diff --git a/src/org/thoughtcrime/securesms/RoutingActivity.java b/src/org/thoughtcrime/securesms/RoutingActivity.java index 0b49223d52..a6c4ac6dd3 100644 --- a/src/org/thoughtcrime/securesms/RoutingActivity.java +++ b/src/org/thoughtcrime/securesms/RoutingActivity.java @@ -109,7 +109,7 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity { } private void handlePushRegistration() { - Intent intent = new Intent(this, RegistrationActivity.class); + Intent intent = getPushRegistrationIntent(); intent.putExtra("next_intent", getConversationListIntent()); startActivity(intent); finish(); @@ -150,7 +150,10 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity { } private Intent getPushRegistrationIntent() { - return new Intent(this, RegistrationActivity.class); + Intent intent = new Intent(this, RegistrationActivity.class); + intent.putExtra("master_secret", masterSecret); + + return intent; } private int getApplicationState() { diff --git a/src/org/thoughtcrime/securesms/crypto/IdentityKey.java b/src/org/thoughtcrime/securesms/crypto/IdentityKey.java index 7814c1b53b..13a43d1762 100644 --- a/src/org/thoughtcrime/securesms/crypto/IdentityKey.java +++ b/src/org/thoughtcrime/securesms/crypto/IdentityKey.java @@ -16,13 +16,12 @@ */ package org.thoughtcrime.securesms.crypto; -import org.bouncycastle.crypto.params.ECPublicKeyParameters; -import org.bouncycastle.math.ec.ECPoint; -import org.thoughtcrime.securesms.util.Hex; - import android.os.Parcel; import android.os.Parcelable; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.thoughtcrime.securesms.util.Hex; + /** * A class for representing an identity key. * @@ -45,7 +44,7 @@ public class IdentityKey implements Parcelable, SerializableKey { } }; - public static final int SIZE = 1 + 33; + public static final int SIZE = 1 + KeyUtil.POINT_SIZE; private static final int VERSION = 1; private ECPublicKeyParameters publicKey; @@ -75,19 +74,8 @@ public class IdentityKey implements Parcelable, SerializableKey { if (version > VERSION) throw new InvalidKeyException("Unsupported key version: " + version); - - byte[] pointBytes = new byte[PublicKey.POINT_SIZE]; - System.arraycopy(bytes, offset+1, pointBytes, 0, pointBytes.length); - - ECPoint Q; - - try { - Q = KeyUtil.decodePoint(pointBytes); - } catch (RuntimeException re) { - throw new InvalidKeyException(re); - } - - this.publicKey = new ECPublicKeyParameters(Q, KeyUtil.domainParameters); + + this.publicKey = KeyUtil.decodePoint(bytes, offset+1); } public byte[] serialize() { diff --git a/src/org/thoughtcrime/securesms/crypto/KeyUtil.java b/src/org/thoughtcrime/securesms/crypto/KeyUtil.java index e5e618af37..d45b0bf08b 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyUtil.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyUtil.java @@ -30,6 +30,7 @@ import org.bouncycastle.math.ec.ECCurve; import org.bouncycastle.math.ec.ECFieldElement; import org.bouncycastle.math.ec.ECPoint; import org.thoughtcrime.securesms.database.keys.LocalKeyRecord; +import org.thoughtcrime.securesms.database.keys.PreKeyRecord; import org.thoughtcrime.securesms.database.keys.RemoteKeyRecord; import org.thoughtcrime.securesms.database.keys.SessionRecord; import org.thoughtcrime.securesms.recipients.Recipient; @@ -55,21 +56,36 @@ public class KeyUtil { private static final ECCurve curve = new ECCurve.Fp(q, a, b); private static final ECPoint g = new ECPoint.Fp(curve, x, y, true); - + + public static final int POINT_SIZE = 33; + public static final ECDomainParameters domainParameters = new ECDomainParameters(curve, g, n); - - public static ECPoint decodePoint(byte[] pointBytes) { - synchronized (curve) { - return curve.decodePoint(pointBytes); - } - } - + public static byte[] encodePoint(ECPoint point) { synchronized (curve) { return point.getEncoded(); } } - + + public static ECPublicKeyParameters decodePoint(byte[] encoded, int offset) + throws InvalidKeyException + { + byte[] pointBytes = new byte[POINT_SIZE]; + System.arraycopy(encoded, offset, pointBytes, 0, pointBytes.length); + + synchronized (curve) { + ECPoint Q; + + try { + Q = curve.decodePoint(pointBytes); + } catch (RuntimeException re) { + throw new InvalidKeyException(re); + } + + return new ECPublicKeyParameters(Q, KeyUtil.domainParameters); + } + } + public static BigInteger calculateAgreement(ECDHBasicAgreement agreement, ECPublicKeyParameters remoteKey) { synchronized (curve) { return agreement.calculateAgreement(remoteKey); diff --git a/src/org/thoughtcrime/securesms/crypto/PreKeyPair.java b/src/org/thoughtcrime/securesms/crypto/PreKeyPair.java new file mode 100644 index 0000000000..717f4510e6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/crypto/PreKeyPair.java @@ -0,0 +1,43 @@ +package org.thoughtcrime.securesms.crypto; + + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.thoughtcrime.securesms.util.Util; + +public class PreKeyPair { + + private final MasterCipher masterCipher; + private final ECPrivateKeyParameters privateKey; + private final ECPublicKeyParameters publicKey; + + public PreKeyPair(MasterSecret masterSecret, AsymmetricCipherKeyPair keyPair) { + this.masterCipher = new MasterCipher(masterSecret); + this.publicKey = (ECPublicKeyParameters)keyPair.getPublic(); + this.privateKey = (ECPrivateKeyParameters)keyPair.getPrivate(); + } + + public PreKeyPair(MasterSecret masterSecret, byte[] serialized) throws InvalidKeyException { + if (serialized.length < KeyUtil.POINT_SIZE + 1) + throw new InvalidKeyException("Serialized length: " + serialized.length); + + byte[] privateKeyBytes = new byte[serialized.length - KeyUtil.POINT_SIZE]; + System.arraycopy(serialized, KeyUtil.POINT_SIZE, privateKeyBytes, 0, privateKeyBytes.length); + + this.masterCipher = new MasterCipher(masterSecret); + this.publicKey = KeyUtil.decodePoint(serialized, 0); + this.privateKey = masterCipher.decryptKey(privateKeyBytes); + } + + public ECPublicKeyParameters getPublicKey() { + return publicKey; + } + + public byte[] serialize() { + byte[] publicKeyBytes = KeyUtil.encodePoint(publicKey.getQ()); + byte[] privateKeyBytes = masterCipher.encryptKey(privateKey); + + return Util.combine(publicKeyBytes, privateKeyBytes); + } +} diff --git a/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java b/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java new file mode 100644 index 0000000000..42d6c5f18e --- /dev/null +++ b/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java @@ -0,0 +1,113 @@ +package org.thoughtcrime.securesms.crypto; + +import android.content.Context; +import android.util.Log; + +import com.google.protobuf.ByteString; +import org.thoughtcrime.securesms.database.keys.InvalidKeyIdException; +import org.thoughtcrime.securesms.database.keys.PreKeyRecord; +import org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity; +import org.whispersystems.textsecure.push.PreKeyList; +import org.whispersystems.textsecure.util.Base64; + +import java.io.File; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.LinkedList; +import java.util.List; + +public class PreKeyUtil { + + public static final int BATCH_SIZE = 70; + + public static List generatePreKeys(Context context, MasterSecret masterSecret) { + List records = new LinkedList(); + long preKeyIdOffset = getNextPreKeyId(context); + + for (int i=0;i getPreKeys(Context context, MasterSecret masterSecret) { + List records = new LinkedList(); + File directory = getPreKeysDirectory(context); + String[] keyRecordIds = directory.list(); + + for (String keyRecordId : keyRecordIds) { + try { + records.add(new PreKeyRecord(context, masterSecret, Long.parseLong(keyRecordId))); + } catch (InvalidKeyIdException e) { + Log.w("PreKeyUtil", e); + new File(getPreKeysDirectory(context), keyRecordId).delete(); + } catch (NumberFormatException nfe) { + Log.w("PreKeyUtil", nfe); + new File(getPreKeysDirectory(context), keyRecordId).delete(); + } + } + + return records; + } + + public static void clearPreKeys(Context context) { + File directory = getPreKeysDirectory(context); + String[] keyRecords = directory.list(); + + for (String keyRecord : keyRecords) { + new File(directory, keyRecord).delete(); + } + } + + public static PreKeyList toJson(List records) { + List encoded = new LinkedList(); + + for (PreKeyRecord record : records) { + PreKeyEntity entity = PreKeyEntity.newBuilder().setId(record.getId()) + .setKey(ByteString.copyFrom(KeyUtil.encodePoint(record.getKeyPair().getPublicKey().getQ()))) + .build(); + + String encodedEntity = Base64.encodeBytesWithoutPadding(entity.toByteArray()); + + encoded.add(encodedEntity); + } + + return new PreKeyList(encoded); + } + + private static long getNextPreKeyId(Context context) { + try { + File directory = getPreKeysDirectory(context); + String[] keyRecordIds = directory.list(); + long nextPreKeyId = 0; + + for (String keyRecordId : keyRecordIds) { + if (Long.parseLong(keyRecordId) > nextPreKeyId) + nextPreKeyId = Long.parseLong(keyRecordId); + } + + if (nextPreKeyId == 0) + nextPreKeyId = SecureRandom.getInstance("SHA1PRNG").nextInt(Integer.MAX_VALUE/2); + + return nextPreKeyId; + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + private static File getPreKeysDirectory(Context context) { + File directory = new File(context.getFilesDir(), PreKeyRecord.PREKEY_DIRECTORY); + + if (!directory.exists()) + directory.mkdirs(); + + return directory; + } + +} diff --git a/src/org/thoughtcrime/securesms/crypto/PublicKey.java b/src/org/thoughtcrime/securesms/crypto/PublicKey.java index eefd2cd96d..ef229c3995 100644 --- a/src/org/thoughtcrime/securesms/crypto/PublicKey.java +++ b/src/org/thoughtcrime/securesms/crypto/PublicKey.java @@ -16,21 +16,19 @@ */ package org.thoughtcrime.securesms.crypto; +import android.util.Log; + +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.thoughtcrime.securesms.util.Hex; +import org.whispersystems.textsecure.util.Conversions; + import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import org.bouncycastle.crypto.params.ECPublicKeyParameters; -import org.bouncycastle.math.ec.ECPoint; -import org.whispersystems.textsecure.util.Conversions; -import org.thoughtcrime.securesms.util.Hex; - -import android.util.Log; - public class PublicKey { - public static final int POINT_SIZE = 33; - public static final int KEY_SIZE = 3 + POINT_SIZE; + public static final int KEY_SIZE = 3 + KeyUtil.POINT_SIZE; - private ECPublicKeyParameters publicKey; + private final ECPublicKeyParameters publicKey; private int id; public PublicKey(PublicKey publicKey) { @@ -50,20 +48,8 @@ public class PublicKey { if ((bytes.length - offset) < KEY_SIZE) throw new InvalidKeyException("Provided bytes are too short."); - this.id = Conversions.byteArrayToMedium(bytes, offset); - byte[] pointBytes = new byte[POINT_SIZE]; - - System.arraycopy(bytes, offset+3, pointBytes, 0, pointBytes.length); - - ECPoint Q; - - try { - Q = KeyUtil.decodePoint(pointBytes); - } catch (RuntimeException re) { - throw new InvalidKeyException(re); - } - - this.publicKey = new ECPublicKeyParameters(Q, KeyUtil.domainParameters); + this.id = Conversions.byteArrayToMedium(bytes, offset); + this.publicKey = KeyUtil.decodePoint(bytes, offset + 3); } public PublicKey(byte[] bytes) throws InvalidKeyException { @@ -99,7 +85,7 @@ public class PublicKey { public byte[] serialize() { byte[] complete = new byte[KEY_SIZE]; byte[] serializedPoint = KeyUtil.encodePoint(publicKey.getQ()); - + Log.w("PublicKey", "Serializing public key point: " + Hex.toString(serializedPoint)); Conversions.mediumToByteArray(complete, 0, id); diff --git a/src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java b/src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java index c46a8897b9..053383b91d 100644 --- a/src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java +++ b/src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java @@ -19,22 +19,18 @@ package org.thoughtcrime.securesms.database.keys; public class InvalidKeyIdException extends Exception { public InvalidKeyIdException() { - // TODO Auto-generated constructor stub } public InvalidKeyIdException(String detailMessage) { super(detailMessage); - // TODO Auto-generated constructor stub } public InvalidKeyIdException(Throwable throwable) { super(throwable); - // TODO Auto-generated constructor stub } public InvalidKeyIdException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); - // TODO Auto-generated constructor stub } } diff --git a/src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java b/src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java index 737b704c22..536997be63 100644 --- a/src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java +++ b/src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java @@ -25,8 +25,6 @@ import org.thoughtcrime.securesms.crypto.KeyUtil; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.CanonicalAddressDatabase; -import org.thoughtcrime.securesms.database.keys.InvalidKeyIdException; -import org.thoughtcrime.securesms.database.keys.Record; import org.thoughtcrime.securesms.recipients.Recipient; import java.io.FileInputStream; @@ -46,7 +44,7 @@ public class LocalKeyRecord extends Record { private final MasterSecret masterSecret; public LocalKeyRecord(Context context, MasterSecret masterSecret, Recipient recipient) { - super(context, getFileNameForRecipient(context, recipient)); + super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); this.masterSecret = masterSecret; this.masterCipher = new MasterCipher(masterSecret); loadData(); @@ -54,11 +52,11 @@ public class LocalKeyRecord extends Record { public static boolean hasRecord(Context context, Recipient recipient) { Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient)); - return Record.hasRecord(context, getFileNameForRecipient(context, recipient)); + return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); } public static void delete(Context context, Recipient recipient) { - Record.delete(context, getFileNameForRecipient(context, recipient)); + Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); } private static String getFileNameForRecipient(Context context, Recipient recipient) { @@ -121,12 +119,11 @@ public class LocalKeyRecord extends Record { synchronized (FILE_LOCK) { try { FileInputStream in = this.openInputStream(); - localCurrentKeyPair = readKeyPair(in); - localNextKeyPair = readKeyPair(in); + localCurrentKeyPair = readKeyPair(in, masterCipher); + localNextKeyPair = readKeyPair(in, masterCipher); in.close(); } catch (FileNotFoundException e) { Log.w("LocalKeyRecord", "No local keypair set found."); - return; } catch (IOException ioe) { Log.w("keyrecord", ioe); // XXX @@ -141,8 +138,11 @@ public class LocalKeyRecord extends Record { writeBlob(keyPairBytes, out); } - private KeyPair readKeyPair(FileInputStream in) throws IOException, InvalidKeyException { + private KeyPair readKeyPair(FileInputStream in, MasterCipher masterCipher) + throws IOException, InvalidKeyException + { byte[] keyPairBytes = readBlob(in); return new KeyPair(keyPairBytes, masterCipher); } + } diff --git a/src/org/thoughtcrime/securesms/database/keys/PreKeyRecord.java b/src/org/thoughtcrime/securesms/database/keys/PreKeyRecord.java new file mode 100644 index 0000000000..d98b07c411 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/keys/PreKeyRecord.java @@ -0,0 +1,125 @@ +package org.thoughtcrime.securesms.database.keys; + +import android.content.Context; +import android.util.Log; + +import org.thoughtcrime.securesms.crypto.InvalidKeyException; +import org.thoughtcrime.securesms.crypto.MasterCipher; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.crypto.PreKeyPair; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; + +public class PreKeyRecord extends Record { + + private static final Object FILE_LOCK = new Object(); + private static final int CURRENT_VERSION_MARKER = 1; + + private final MasterCipher masterCipher; + private final MasterSecret masterSecret; + + private PreKeyPair keyPair; + private long id; + + public PreKeyRecord(Context context, MasterSecret masterSecret, long id) + throws InvalidKeyIdException + { + super(context, PREKEY_DIRECTORY, id+""); + + this.id = id; + this.masterSecret = masterSecret; + this.masterCipher = new MasterCipher(masterSecret); + + loadData(); + } + + public PreKeyRecord(Context context, MasterSecret masterSecret, + long id, PreKeyPair keyPair) + { + super(context, PREKEY_DIRECTORY, id+""); + this.id = id; + this.keyPair = keyPair; + this.masterSecret = masterSecret; + this.masterCipher = new MasterCipher(masterSecret); + } + + public long getId() { + return id; + } + + public PreKeyPair getKeyPair() { + return keyPair; + } + + public static boolean hasRecord(Context context, long id) { + Log.w("PreKeyRecord", "Checking: " + id); + return Record.hasRecord(context, PREKEY_DIRECTORY, id+""); + } + + public static void delete(Context context, long id) { + Record.delete(context, PREKEY_DIRECTORY, id+""); + } + + public void save() { + synchronized (FILE_LOCK) { + try { + RandomAccessFile file = openRandomAccessFile(); + FileChannel out = file.getChannel(); + out.position(0); + + writeInteger(CURRENT_VERSION_MARKER, out); + writeKeyPair(keyPair, out); + + out.force(true); + out.truncate(out.position()); + out.close(); + file.close(); + } catch (IOException ioe) { + Log.w("PreKeyRecord", ioe); + } + } + } + + private void loadData() throws InvalidKeyIdException { + synchronized (FILE_LOCK) { + try { + FileInputStream in = this.openInputStream(); + int recordVersion = readInteger(in); + + if (recordVersion != CURRENT_VERSION_MARKER) { + Log.w("PreKeyRecord", "Invalid version: " + recordVersion); + return; + } + + keyPair = readKeyPair(in, masterCipher); + in.close(); + } catch (FileNotFoundException e) { + Log.w("PreKeyRecord", e); + throw new InvalidKeyIdException(e); + } catch (IOException ioe) { + Log.w("PreKeyRecord", ioe); + throw new InvalidKeyIdException(ioe); + } catch (InvalidKeyException ike) { + Log.w("LocalKeyRecord", ike); + throw new InvalidKeyIdException(ike); + } + } + } + + private void writeKeyPair(PreKeyPair keyPair, FileChannel out) throws IOException { + byte[] serialized = keyPair.serialize(); + writeBlob(serialized, out); + } + + private PreKeyPair readKeyPair(FileInputStream in, MasterCipher masterCipher) + throws IOException, InvalidKeyException + { + byte[] keyPairBytes = readBlob(in); + return new PreKeyPair(masterSecret, keyPairBytes); + } + +} diff --git a/src/org/thoughtcrime/securesms/database/keys/Record.java b/src/org/thoughtcrime/securesms/database/keys/Record.java index 9d53eb424c..fc8ba8d499 100644 --- a/src/org/thoughtcrime/securesms/database/keys/Record.java +++ b/src/org/thoughtcrime/securesms/database/keys/Record.java @@ -18,6 +18,9 @@ package org.thoughtcrime.securesms.database.keys; import android.content.Context; +import org.thoughtcrime.securesms.crypto.InvalidKeyException; +import org.thoughtcrime.securesms.crypto.KeyPair; +import org.thoughtcrime.securesms.crypto.MasterCipher; import org.whispersystems.textsecure.util.Conversions; import java.io.File; @@ -30,24 +33,29 @@ import java.nio.channels.FileChannel; public abstract class Record { + protected static final String SESSIONS_DIRECTORY = "sessions"; + public static final String PREKEY_DIRECTORY = "prekeys"; + protected final String address; + protected final String directory; protected final Context context; - public Record(Context context, String address) { - this.context = context; - this.address = address; + public Record(Context context, String directory, String address) { + this.context = context; + this.directory = directory; + this.address = address; } public void delete() { - delete(this.context, this.address); + delete(this.context, this.directory, this.address); } - protected static void delete(Context context, String address) { - getAddressFile(context, address).delete(); + protected static void delete(Context context, String directory, String address) { + getAddressFile(context, directory, address).delete(); } - protected static boolean hasRecord(Context context, String address) { - return getAddressFile(context, address).exists(); + protected static boolean hasRecord(Context context, String directory, String address) { + return getAddressFile(context, directory, address).exists(); } protected RandomAccessFile openRandomAccessFile() throws FileNotFoundException { @@ -59,11 +67,11 @@ public abstract class Record { } private File getAddressFile() { - return getAddressFile(context, address); + return getAddressFile(context, directory, address); } - private static File getAddressFile(Context context, String address) { - return new File(context.getFilesDir().getAbsolutePath() + File.separatorChar + "sessions", address); + private static File getAddressFile(Context context, String directory, String address) { + return new File(context.getFilesDir().getAbsolutePath() + File.separatorChar + directory, address); } protected byte[] readBlob(FileInputStream in) throws IOException { diff --git a/src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java b/src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java index 71470bf70e..bf8ede2bf7 100644 --- a/src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java +++ b/src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java @@ -47,17 +47,17 @@ public class RemoteKeyRecord extends Record { private PublicKey remoteKeyLast; public RemoteKeyRecord(Context context, Recipient recipient) { - super(context,getFileNameForRecipient(context, recipient)); + super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); loadData(); } public static void delete(Context context, Recipient recipient) { - Record.delete(context, getFileNameForRecipient(context, recipient)); + Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); } public static boolean hasRecord(Context context, Recipient recipient) { Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient)); - return Record.hasRecord(context, getFileNameForRecipient(context, recipient)); + return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); } private static String getFileNameForRecipient(Context context, Recipient recipient) { @@ -126,7 +126,6 @@ public class RemoteKeyRecord extends Record { in.close(); } catch (FileNotFoundException e) { Log.w("RemoteKeyRecord", "No remote keys found."); - return; } catch (IOException ioe) { Log.w("keyrecord", ioe); // XXX diff --git a/src/org/thoughtcrime/securesms/database/keys/SessionRecord.java b/src/org/thoughtcrime/securesms/database/keys/SessionRecord.java index 5c3ac4e802..49a651fec9 100644 --- a/src/org/thoughtcrime/securesms/database/keys/SessionRecord.java +++ b/src/org/thoughtcrime/securesms/database/keys/SessionRecord.java @@ -58,19 +58,19 @@ public class SessionRecord extends Record { } public SessionRecord(Context context, MasterSecret masterSecret, long recipientId) { - super(context, recipientId+""); + super(context, SESSIONS_DIRECTORY, recipientId+""); this.masterSecret = masterSecret; this.sessionVersion = 31337; loadData(); } public static void delete(Context context, Recipient recipient) { - Record.delete(context, getRecipientId(context, recipient)+""); + Record.delete(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient)+""); } public static boolean hasSession(Context context, Recipient recipient) { Log.w("LocalKeyRecord", "Checking: " + getRecipientId(context, recipient)); - return Record.hasRecord(context, getRecipientId(context, recipient)+""); + return Record.hasRecord(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient)+""); } private static long getRecipientId(Context context, Recipient recipient) { diff --git a/src/org/thoughtcrime/securesms/encoded/PreKeyProtos.java b/src/org/thoughtcrime/securesms/encoded/PreKeyProtos.java new file mode 100644 index 0000000000..2694524abb --- /dev/null +++ b/src/org/thoughtcrime/securesms/encoded/PreKeyProtos.java @@ -0,0 +1,451 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: PreKeyEntity.proto + +package org.thoughtcrime.securesms.encoded; + +public final class PreKeyProtos { + private PreKeyProtos() {} + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistry registry) { + } + public interface PreKeyEntityOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // optional uint64 id = 1; + boolean hasId(); + long getId(); + + // optional bytes key = 2; + boolean hasKey(); + com.google.protobuf.ByteString getKey(); + } + public static final class PreKeyEntity extends + com.google.protobuf.GeneratedMessage + implements PreKeyEntityOrBuilder { + // Use PreKeyEntity.newBuilder() to construct. + private PreKeyEntity(Builder builder) { + super(builder); + } + private PreKeyEntity(boolean noInit) {} + + private static final PreKeyEntity defaultInstance; + public static PreKeyEntity getDefaultInstance() { + return defaultInstance; + } + + public PreKeyEntity getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.thoughtcrime.securesms.encoded.PreKeyProtos.internal_static_textsecure_PreKeyEntity_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.thoughtcrime.securesms.encoded.PreKeyProtos.internal_static_textsecure_PreKeyEntity_fieldAccessorTable; + } + + private int bitField0_; + // optional uint64 id = 1; + public static final int ID_FIELD_NUMBER = 1; + private long id_; + public boolean hasId() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public long getId() { + return id_; + } + + // optional bytes key = 2; + public static final int KEY_FIELD_NUMBER = 2; + private com.google.protobuf.ByteString key_; + public boolean hasKey() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public com.google.protobuf.ByteString getKey() { + return key_; + } + + private void initFields() { + id_ = 0L; + key_ = com.google.protobuf.ByteString.EMPTY; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeUInt64(1, id_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeBytes(2, key_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(1, id_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(2, key_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntityOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.thoughtcrime.securesms.encoded.PreKeyProtos.internal_static_textsecure_PreKeyEntity_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.thoughtcrime.securesms.encoded.PreKeyProtos.internal_static_textsecure_PreKeyEntity_fieldAccessorTable; + } + + // Construct using org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + id_ = 0L; + bitField0_ = (bitField0_ & ~0x00000001); + key_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity.getDescriptor(); + } + + public org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity getDefaultInstanceForType() { + return org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity.getDefaultInstance(); + } + + public org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity build() { + org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity buildPartial() { + org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity result = new org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.id_ = id_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.key_ = key_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity) { + return mergeFrom((org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity other) { + if (other == org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity.getDefaultInstance()) return this; + if (other.hasId()) { + setId(other.getId()); + } + if (other.hasKey()) { + setKey(other.getKey()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 8: { + bitField0_ |= 0x00000001; + id_ = input.readUInt64(); + break; + } + case 18: { + bitField0_ |= 0x00000002; + key_ = input.readBytes(); + break; + } + } + } + } + + private int bitField0_; + + // optional uint64 id = 1; + private long id_ ; + public boolean hasId() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public long getId() { + return id_; + } + public Builder setId(long value) { + bitField0_ |= 0x00000001; + id_ = value; + onChanged(); + return this; + } + public Builder clearId() { + bitField0_ = (bitField0_ & ~0x00000001); + id_ = 0L; + onChanged(); + return this; + } + + // optional bytes key = 2; + private com.google.protobuf.ByteString key_ = com.google.protobuf.ByteString.EMPTY; + public boolean hasKey() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public com.google.protobuf.ByteString getKey() { + return key_; + } + public Builder setKey(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + key_ = value; + onChanged(); + return this; + } + public Builder clearKey() { + bitField0_ = (bitField0_ & ~0x00000002); + key_ = getDefaultInstance().getKey(); + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:textsecure.PreKeyEntity) + } + + static { + defaultInstance = new PreKeyEntity(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:textsecure.PreKeyEntity) + } + + private static com.google.protobuf.Descriptors.Descriptor + internal_static_textsecure_PreKeyEntity_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_textsecure_PreKeyEntity_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor + getDescriptor() { + return descriptor; + } + private static com.google.protobuf.Descriptors.FileDescriptor + descriptor; + static { + java.lang.String[] descriptorData = { + "\n\022PreKeyEntity.proto\022\ntextsecure\"\'\n\014PreK" + + "eyEntity\022\n\n\002id\030\001 \001(\004\022\013\n\003key\030\002 \001(\014B2\n\"org" + + ".thoughtcrime.securesms.encodedB\014PreKeyP" + + "rotos" + }; + com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = + new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { + public com.google.protobuf.ExtensionRegistry assignDescriptors( + com.google.protobuf.Descriptors.FileDescriptor root) { + descriptor = root; + internal_static_textsecure_PreKeyEntity_descriptor = + getDescriptor().getMessageTypes().get(0); + internal_static_textsecure_PreKeyEntity_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_textsecure_PreKeyEntity_descriptor, + new java.lang.String[] { "Id", "Key", }, + org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity.class, + org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity.Builder.class); + return null; + } + }; + com.google.protobuf.Descriptors.FileDescriptor + .internalBuildGeneratedFileFrom(descriptorData, + new com.google.protobuf.Descriptors.FileDescriptor[] { + }, assigner); + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/src/org/thoughtcrime/securesms/service/RegistrationService.java b/src/org/thoughtcrime/securesms/service/RegistrationService.java index ba78172a0f..6370865a8f 100644 --- a/src/org/thoughtcrime/securesms/service/RegistrationService.java +++ b/src/org/thoughtcrime/securesms/service/RegistrationService.java @@ -13,6 +13,9 @@ import android.util.Pair; import com.google.android.gcm.GCMRegistrar; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.crypto.PreKeyUtil; +import org.thoughtcrime.securesms.database.keys.PreKeyRecord; import org.thoughtcrime.securesms.gcm.GcmIntentService; import org.thoughtcrime.securesms.gcm.GcmRegistrationTimeoutException; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -23,6 +26,7 @@ import org.whispersystems.textsecure.util.Util; import java.io.File; import java.io.IOException; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -61,6 +65,7 @@ public class RegistrationService extends Service { public static final String GCM_REGISTRATION_ID = "GCMRegistrationId"; private static final long REGISTRATION_TIMEOUT_MILLIS = 120000; + private static final Object GENERATING_PREKEYS_SEMAPHOR = new Object(); private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final Binder binder = new RegistrationServiceBinder(); @@ -73,6 +78,7 @@ public class RegistrationService extends Service { private String challenge; private String gcmRegistrationId; private long verificationStartTime; + private boolean generatingPreKeys; @Override public int onStartCommand(final Intent intent, int flags, int startId) { @@ -80,9 +86,9 @@ public class RegistrationService extends Service { executor.execute(new Runnable() { @Override public void run() { - if (intent.getAction().equals(REGISTER_NUMBER_ACTION)) handleRegistrationIntent(intent); + if (intent.getAction().equals(REGISTER_NUMBER_ACTION)) handleSmsRegistrationIntent(intent); else if (intent.getAction().equals(VOICE_REQUESTED_ACTION)) handleVoiceRequestedIntent(intent); - else if (intent.getAction().equals(VOICE_REGISTER_ACTION)) handleVoiceRegisterIntent(intent); + else if (intent.getAction().equals(VOICE_REGISTER_ACTION)) handleVoiceRegistrationIntent(intent); } }); } @@ -135,6 +141,26 @@ public class RegistrationService extends Service { registerReceiver(gcmRegistrationReceiver, filter); } + private void initializePreKeyGenerator(final MasterSecret masterSecret) { + synchronized (GENERATING_PREKEYS_SEMAPHOR) { + if (generatingPreKeys) return; + else generatingPreKeys = true; + } + + new Thread() { + public void run() { + if (PreKeyUtil.getPreKeys(RegistrationService.this, masterSecret).size() < PreKeyUtil.BATCH_SIZE) { + PreKeyUtil.generatePreKeys(RegistrationService.this, masterSecret); + } + + synchronized (GENERATING_PREKEYS_SEMAPHOR) { + generatingPreKeys = false; + GENERATING_PREKEYS_SEMAPHOR.notifyAll(); + } + } + }.start(); + } + private synchronized void shutdownChallengeListener() { if (challengeReceiver != null) { unregisterReceiver(challengeReceiver); @@ -155,24 +181,20 @@ public class RegistrationService extends Service { intent.getStringExtra("password"))); } - private void handleVoiceRegisterIntent(Intent intent) { + private void handleVoiceRegistrationIntent(Intent intent) { markAsVerifying(true); - String number = intent.getStringExtra("e164number"); - String password = intent.getStringExtra("password"); + String number = intent.getStringExtra("e164number"); + String password = intent.getStringExtra("password" ); + MasterSecret masterSecret = intent.getParcelableExtra("master_secret"); try { initializeGcmRegistrationListener(); + initializePreKeyGenerator(masterSecret); PushServiceSocket socket = new PushServiceSocket(this, number, password); - setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number)); - GCMRegistrar.register(this, GcmIntentService.GCM_SENDER_ID); - String gcmRegistrationId = waitForGcmRegistrationId(); - - socket.registerGcmId(gcmRegistrationId); - Pair directory = socket.retrieveDirectory(); - NumberFilter.getInstance(this).update(directory.first, directory.second); + handleCommonRegistration(masterSecret, socket, number); markAsVerified(number, password); @@ -195,15 +217,17 @@ public class RegistrationService extends Service { } } - private void handleRegistrationIntent(Intent intent) { + private void handleSmsRegistrationIntent(Intent intent) { markAsVerifying(true); - String number = intent.getStringExtra("e164number"); + String number = intent.getStringExtra("e164number"); + MasterSecret masterSecret = intent.getParcelableExtra("master_secret"); try { String password = Util.getSecret(18); initializeChallengeListener(); initializeGcmRegistrationListener(); + initializePreKeyGenerator(masterSecret); setState(new RegistrationState(RegistrationState.STATE_CONNECTING, number)); PushServiceSocket socket = new PushServiceSocket(this, number, password); @@ -213,14 +237,7 @@ public class RegistrationService extends Service { String challenge = waitForChallenge(); socket.verifyAccount(challenge); - setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number)); - GCMRegistrar.register(this, GcmIntentService.GCM_SENDER_ID); - String gcmRegistrationId = waitForGcmRegistrationId(); - - socket.registerGcmId(gcmRegistrationId); - Pair directory = socket.retrieveDirectory(); - NumberFilter.getInstance(this).update(directory.first, directory.second); - + handleCommonRegistration(masterSecret, socket, number); markAsVerified(number, password); setState(new RegistrationState(RegistrationState.STATE_COMPLETE, number)); @@ -247,6 +264,22 @@ public class RegistrationService extends Service { } } + private void handleCommonRegistration(MasterSecret masterSecret, PushServiceSocket socket, String number) + throws GcmRegistrationTimeoutException, IOException + { + setState(new RegistrationState(RegistrationState.STATE_GENERATING_KEYS, number)); + List records = waitForPreKeys(masterSecret); + socket.registerPreKeys(PreKeyUtil.toJson(records)); + + setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number)); + GCMRegistrar.register(this, GcmIntentService.GCM_SENDER_ID); + String gcmRegistrationId = waitForGcmRegistrationId(); + + socket.registerGcmId(gcmRegistrationId); + Pair directory = socket.retrieveDirectory(); + NumberFilter.getInstance(this).update(directory.first, directory.second); + } + private synchronized String waitForChallenge() throws AccountVerificationTimeoutException { this.verificationStartTime = System.currentTimeMillis(); @@ -279,6 +312,20 @@ public class RegistrationService extends Service { return this.gcmRegistrationId; } + private List waitForPreKeys(MasterSecret masterSecret) { + synchronized (GENERATING_PREKEYS_SEMAPHOR) { + while (generatingPreKeys) { + try { + GENERATING_PREKEYS_SEMAPHOR.wait(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + } + } + + return PreKeyUtil.getPreKeys(this, masterSecret); + } + private synchronized void challengeReceived(String challenge) { this.challenge = challenge; notifyAll(); @@ -367,6 +414,7 @@ public class RegistrationService extends Service { public static final int STATE_GCM_TIMEOUT = 10; public static final int STATE_VOICE_REQUESTED = 12; + public static final int STATE_GENERATING_KEYS = 13; public final int state; public final String number;