Switch to a more heavily TOFU model for identity keys.

1) There is no longer a concept of "verified" or "unverified."
   Only "what we saw last time" and "different from last time."

2) Let's eliminate "verify session," since we're all about
   identity keys now.

3) Mark manually processed key exchanges as processed.
This commit is contained in:
Moxie Marlinspike 2013-05-23 16:36:24 -07:00
parent ef7977128b
commit 24fc93e9ae
33 changed files with 497 additions and 1019 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,59 +1,48 @@
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView android:id="@+id/ScrollView" android:layout_width="fill_parent"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:layout_height="fill_parent" android:fillViewport="true"
xmlns:android="http://schemas.android.com/apk/res/android"> android:background="@drawable/background_pattern_repeat">
<LinearLayout <FrameLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="fill_parent"
android:orientation="vertical" android:gravity="center">
android:padding="10dip">
<TextView android:id="@+id/description_text" <LinearLayout android:paddingRight="16dip"
android:layout_width="fill_parent" android:paddingLeft="16dip"
android:layout_height="wrap_content" android:paddingTop="10dip"
android:textSize="17dip" android:layout_width="fill_parent"
android:layout_marginBottom="5dip" /> android:layout_height="wrap_content"
android:layout_gravity="center"
<TextView android:id="@+id/signature_text" android:orientation="vertical">
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="17dip"
android:textStyle="italic"
android:layout_marginBottom="10dip" />
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/verify_session_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/receive_key_activity__session"
android:visibility="gone"
android:gravity="center" />
<Button android:id="@+id/verify_identity_button" <TextView style="@style/Registration.Description"
android:layout_width="wrap_content" android:id="@+id/description_text"
android:layout_height="wrap_content" android:layout_width="fill_parent"
android:text="@string/receive_key_activity__identities" android:layout_marginBottom="16dip"
android:visibility="gone" android:layout_marginTop="16dip"
android:gravity="center" /> android:text="@string/receive_key_activity__the_signature_on_this_key_exchange_is_different"/>
<Button android:id="@+id/ok_button" <LinearLayout android:layout_height="wrap_content"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_marginBottom="7dip"
android:text="@string/receive_key_activity__complete_exchange" android:orientation="horizontal">
android:gravity="center"/>
<Button android:id="@+id/cancel_button"
<Button android:id="@+id/cancel_button" android:text="@android:string/cancel"
android:text="@android:string/cancel" android:layout_width="0dp"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_weight="1"/>
android:gravity="center"/>
</LinearLayout> <Button android:id="@+id/ok_button"
</LinearLayout> android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/receive_key_activity__complete"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</ScrollView> </ScrollView>

View file

@ -1,24 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <fragment android:id="@+id/fragment_content"
android:layout_width="fill_parent" android:name="org.thoughtcrime.securesms.ReviewIdentitiesFragment"
android:layout_height="fill_parent" android:layout_width="match_parent"
android:orientation="vertical" android:layout_height="match_parent"/>
android:paddingLeft="16dip" </FrameLayout>
android:paddingRight="16dip">
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:drawSelectorOnTop="false"
android:scrollbarStyle="insideOverlay"
android:fadingEdgeLength="16dip" />
<TextView android:id="@id/android:empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/review_identities__you_don_t_currently_have_any_identity_keys_in_your_trust_database"
android:textAppearance="?android:attr/textAppearanceMedium"
android:padding="20dip" />
</LinearLayout>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:paddingLeft="16dip"
android:paddingRight="16dip">
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:drawSelectorOnTop="false"
android:scrollbarStyle="insideOverlay"
android:fadingEdgeLength="16dip" />
<TextView android:id="@id/android:empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/review_identities__you_don_t_currently_have_any_identity_keys_in_your_trust_database"
android:textAppearance="?android:attr/textAppearanceMedium"
android:padding="20dip" />
</LinearLayout>

View file

@ -2,7 +2,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="Security" <item android:title="Security"
android:id="@+id/menu_security" android:id="@+id/menu_security"
android:icon="@drawable/ic_menu_lock_holo_dark" android:icon="@drawable/ic_menu_unlock_holo_dark"
android:showAsAction="ifRoom"> android:showAsAction="ifRoom">
<menu> <menu>
<item android:title="@string/conversation_insecure__menu_start_secure_session" <item android:title="@string/conversation_insecure__menu_start_secure_session"

View file

@ -2,12 +2,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/conversation_secure_verified__menu_security" <item android:title="@string/conversation_secure_verified__menu_security"
android:id="@+id/menu_security" android:id="@+id/menu_security"
android:icon="@drawable/ic_menu_lock_verified_holo_dark" android:icon="@drawable/ic_menu_lock_holo_dark"
android:showAsAction="ifRoom"> android:showAsAction="ifRoom">
<menu> <menu>
<item android:title="@string/conversation_secure_verified__menu_verify_session"
android:id="@+id/menu_verify_session" />
<item android:title="@string/conversation_secure_verified__menu_verify_recipient" <item android:title="@string/conversation_secure_verified__menu_verify_recipient"
android:id="@+id/menu_verify_recipient"/> android:id="@+id/menu_verify_recipient"/>

View file

@ -2,15 +2,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/conversation_secure_verified__menu_security" <item android:title="@string/conversation_secure_verified__menu_security"
android:id="@+id/menu_security" android:id="@+id/menu_security"
android:icon="@drawable/ic_menu_lock_unverified_holo_dark" android:icon="@drawable/ic_menu_lock_holo_dark"
android:showAsAction="ifRoom"> android:showAsAction="ifRoom">
<menu> <menu>
<item android:title="@string/conversation_secure_verified__menu_verify_session" <item android:title="@string/conversation_secure_verified__menu_verify_session"
android:id="@+id/menu_verify_session" /> android:id="@+id/menu_verify_session" />
<item android:title="@string/conversation_secure_verified__menu_verify_recipient"
android:id="@+id/menu_verify_recipient"/>
<item android:title="@string/conversation_secure_verified__menu_abort_secure_session" <item android:title="@string/conversation_secure_verified__menu_abort_secure_session"
android:id="@+id/menu_abort_session"/> android:id="@+id/menu_abort_session"/>

View file

@ -316,8 +316,13 @@
<!-- receive_key_activity --> <!-- receive_key_activity -->
<string name="receive_key_activity__session">Session</string> <string name="receive_key_activity__session">Session</string>
<string name="receive_key_activity__identities">Identities</string> <string name="receive_key_activity__identities">Identities</string>
<string name="receive_key_activity__complete_exchange">Complete Exchange</string> <string name="receive_key_activity__complete">Complete</string>
<string name="receive_key_activity__the_signature_on_this_key_exchange_is_different">The
signature on this key exchange is different than what you\'ve previously received from this
contact. This could either mean that someone is trying to intercept your communication, or
that this contact simply re-installed TextSecure and now has a new identity key.
</string>
<!-- recipients_panel --> <!-- recipients_panel -->
<string name="recipients_panel__to">To</string> <string name="recipients_panel__to">To</string>
@ -473,7 +478,6 @@
<string name="conversation_list__menu_search">Search</string> <string name="conversation_list__menu_search">Search</string>
<!-- conversation_secure_verified --> <!-- conversation_secure_verified -->
<!-- conversation_secure_unverified -->
<string name="conversation_secure_verified__menu_security">Security</string> <string name="conversation_secure_verified__menu_security">Security</string>
<string name="conversation_secure_verified__menu_verify_session">Verify Session</string> <string name="conversation_secure_verified__menu_verify_session">Verify Session</string>
<string name="conversation_secure_verified__menu_verify_recipient">Verify Recipient</string> <string name="conversation_secure_verified__menu_verify_recipient">Verify Recipient</string>
@ -508,6 +512,9 @@
<!-- verify_keys --> <!-- verify_keys -->
<string name="verify_keys__menu_verified">Verified</string> <string name="verify_keys__menu_verified">Verified</string>
<string name="ReceiveKeyActivity_you_may_wish_to_verify_this_contact">You may wish to verify
this contact.
</string>
<!-- Misc. piggybacking --> <!-- Misc. piggybacking -->

View file

@ -123,14 +123,6 @@
android:title="@string/preferences__view_my_identity_key" android:title="@string/preferences__view_my_identity_key"
android:summary="@string/preferences__view_my_identity_key"/> android:summary="@string/preferences__view_my_identity_key"/>
<Preference android:key="pref_export_identity"
android:title="@string/preferences__export_my_identity_key"
android:summary="@string/preferences__export_my_identity_key"/>
<Preference android:key="pref_import_identity"
android:title="@string/preferences__import_contacts_key"
android:summary="@string/preferences__import_an_identity_key_from_a_contact"/>
<Preference android:key="pref_manage_identity" <Preference android:key="pref_manage_identity"
android:title="@string/preferences__manage_identity_keys" android:title="@string/preferences__manage_identity_keys"
android:summary="@string/preferences__manage_configured_identity_keys"/> android:summary="@string/preferences__manage_configured_identity_keys"/>

View file

@ -55,7 +55,6 @@ import java.util.List;
public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPreferenceActivity { public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPreferenceActivity {
private static final int PICK_IDENTITY_CONTACT = 1; private static final int PICK_IDENTITY_CONTACT = 1;
private static final int IMPORT_IDENTITY_ID = 2;
public static final String RINGTONE_PREF = "pref_key_ringtone"; public static final String RINGTONE_PREF = "pref_key_ringtone";
public static final String VIBRATE_PREF = "pref_key_vibrate"; public static final String VIBRATE_PREF = "pref_key_vibrate";
@ -107,10 +106,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
this.findPreference(VIEW_MY_IDENTITY_PREF) this.findPreference(VIEW_MY_IDENTITY_PREF)
.setOnPreferenceClickListener(new ViewMyIdentityClickListener()); .setOnPreferenceClickListener(new ViewMyIdentityClickListener());
this.findPreference(EXPORT_MY_IDENTITY_PREF)
.setOnPreferenceClickListener(new ExportMyIdentityClickListener());
this.findPreference(IMPORT_CONTACT_IDENTITY_PREF)
.setOnPreferenceClickListener(new ImportContactIdentityClickListener());
this.findPreference(MANAGE_IDENTITIES_PREF) this.findPreference(MANAGE_IDENTITIES_PREF)
.setOnPreferenceClickListener(new ManageIdentitiesClickListener()); .setOnPreferenceClickListener(new ManageIdentitiesClickListener());
this.findPreference(CHANGE_PASSPHRASE_PREF) this.findPreference(CHANGE_PASSPHRASE_PREF)
@ -133,8 +128,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
switch (reqCode) { switch (reqCode) {
case (PICK_IDENTITY_CONTACT) : handleIdentitySelection(data); break; case PICK_IDENTITY_CONTACT: handleIdentitySelection(data); break;
case IMPORT_IDENTITY_ID: importIdentityKey(data.getData()); break;
} }
} }
} }
@ -209,25 +203,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
} }
} }
private void importIdentityKey(Uri uri) {
IdentityKey identityKey = ContactAccessor.getInstance().importIdentityKey(this, uri);
String contactName = ContactAccessor.getInstance().getNameFromContact(this, uri);
if (identityKey == null) {
Dialogs.displayAlert(this,
getString(R.string.ApplicationPreferenceActivity_not_found_exclamation),
getString(R.string.ApplicationPreferenceActivity_no_valid_identity_key_was_found_in_the_specified_contact),
android.R.drawable.ic_dialog_alert);
return;
}
Intent verifyImportedKeyIntent = new Intent(this, VerifyImportedIdentityActivity.class);
verifyImportedKeyIntent.putExtra("master_secret", getIntent().getParcelableExtra("master_secret"));
verifyImportedKeyIntent.putExtra("identity_key", identityKey);
verifyImportedKeyIntent.putExtra("contact_name", contactName);
startActivity(verifyImportedKeyIntent);
}
private class IdentityPreferenceClickListener implements Preference.OnPreferenceClickListener { private class IdentityPreferenceClickListener implements Preference.OnPreferenceClickListener {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
@ -249,57 +224,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
} }
} }
private class ExportMyIdentityClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
if (!IdentityKeyUtil.hasIdentityKey(ApplicationPreferencesActivity.this)) {
Toast.makeText(ApplicationPreferencesActivity.this,
R.string.ApplicationPreferenceActivity_you_don_t_have_an_identity_key_exclamation,
Toast.LENGTH_LONG).show();
return true;
}
List<Long> rawContactIds = ContactIdentityManager
.getInstance(ApplicationPreferencesActivity.this)
.getSelfIdentityRawContactIds();
if (rawContactIds== null) {
Toast.makeText(ApplicationPreferencesActivity.this,
R.string.ApplicationPreferenceActivity_you_have_not_yet_defined_a_contact_for_yourself,
Toast.LENGTH_LONG).show();
return true;
}
ContactAccessor.getInstance().insertIdentityKey(ApplicationPreferencesActivity.this, rawContactIds,
IdentityKeyUtil.getIdentityKey(ApplicationPreferencesActivity.this));
Toast.makeText(ApplicationPreferencesActivity.this,
R.string.ApplicationPreferenceActivity_exported_to_contacts_database,
Toast.LENGTH_LONG).show();
return true;
}
}
private class ImportContactIdentityClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
MasterSecret masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret");
if (masterSecret != null) {
Intent importIntent = new Intent(Intent.ACTION_PICK);
importIntent.setType(ContactsContract.Contacts.CONTENT_TYPE);
startActivityForResult(importIntent, IMPORT_IDENTITY_ID);
} else {
Toast.makeText(ApplicationPreferencesActivity.this,
R.string.ApplicationPreferenceActivity_you_need_to_have_entered_your_passphrase_before_importing_keys,
Toast.LENGTH_LONG).show();
}
return true;
}
}
private class ManageIdentitiesClickListener implements Preference.OnPreferenceClickListener { private class ManageIdentitiesClickListener implements Preference.OnPreferenceClickListener {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {

View file

@ -48,7 +48,6 @@ import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem; import com.actionbarsherlock.view.MenuItem;
import org.thoughtcrime.securesms.components.RecipientsPanel; import org.thoughtcrime.securesms.components.RecipientsPanel;
import org.thoughtcrime.securesms.crypto.AuthenticityCalculator;
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator; import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.KeyUtil; import org.thoughtcrime.securesms.crypto.KeyUtil;
@ -125,6 +124,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
private long threadId; private long threadId;
private int distributionType; private int distributionType;
private boolean isEncryptedConversation; private boolean isEncryptedConversation;
private boolean isAuthenticatedConversation;
private boolean isMmsEnabled = true; private boolean isMmsEnabled = true;
private CharacterCalculator characterCalculator = new CharacterCalculator(); private CharacterCalculator characterCalculator = new CharacterCalculator();
@ -211,10 +211,10 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
if (isSingleConversation() && isEncryptedConversation) if (isSingleConversation() && isEncryptedConversation)
{ {
if (isAuthenticatedSession()) { if (isAuthenticatedConversation) {
inflater.inflate(R.menu.conversation_secure_verified, menu); inflater.inflate(R.menu.conversation_secure_identity, menu);
} else { } else {
inflater.inflate(R.menu.conversation_secure_unverified, menu); inflater.inflate(R.menu.conversation_secure_no_identity, menu);
} }
} else if (isSingleConversation()) { } else if (isSingleConversation()) {
inflater.inflate(R.menu.conversation_insecure, menu); inflater.inflate(R.menu.conversation_insecure, menu);
@ -435,16 +435,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
String subtitle = null; String subtitle = null;
if (isSingleConversation()) { if (isSingleConversation()) {
title = getRecipients().getPrimaryRecipient().getName();
if (isEncryptedConversation) {
title = AuthenticityCalculator.getAuthenticatedName(this,
getRecipients().getPrimaryRecipient(),
masterSecret);
}
if (title == null || title.trim().length() == 0) {
title = getRecipients().getPrimaryRecipient().getName();
}
if (title == null || title.trim().length() == 0) { if (title == null || title.trim().length() == 0) {
title = getRecipients().getPrimaryRecipient().getNumber(); title = getRecipients().getPrimaryRecipient().getNumber();
@ -512,12 +503,14 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
KeyUtil.isSessionFor(this, getRecipients().getPrimaryRecipient())) KeyUtil.isSessionFor(this, getRecipients().getPrimaryRecipient()))
{ {
sendButton.setImageResource(R.drawable.ic_send_encrypted_holo_light); sendButton.setImageResource(R.drawable.ic_send_encrypted_holo_light);
this.isEncryptedConversation = true; this.isEncryptedConversation = true;
this.characterCalculator = new EncryptedCharacterCalculator(); this.isAuthenticatedConversation = KeyUtil.isIdentityKeyFor(this, masterSecret, getRecipients().getPrimaryRecipient());
this.characterCalculator = new EncryptedCharacterCalculator();
} else { } else {
sendButton.setImageResource(R.drawable.ic_send_holo_light); sendButton.setImageResource(R.drawable.ic_send_holo_light);
this.isEncryptedConversation = false; this.isEncryptedConversation = false;
this.characterCalculator = new CharacterCalculator(); this.isAuthenticatedConversation = false;
this.characterCalculator = new CharacterCalculator();
} }
calculateCharactersRemaining(); calculateCharactersRemaining();
@ -728,12 +721,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
return getRecipients() != null && !getRecipients().isSingleRecipient(); return getRecipients() != null && !getRecipients().isSingleRecipient();
} }
private boolean isAuthenticatedSession() {
return AuthenticityCalculator.isAuthenticated(this,
getRecipients().getPrimaryRecipient(),
masterSecret);
}
private Recipients getRecipients() { private Recipients getRecipients() {
try { try {
if (isExistingConversation()) return this.recipients; if (isExistingConversation()) return this.recipients;

View file

@ -322,6 +322,7 @@ public class ConversationItem extends LinearLayout {
intent.putExtra("recipient", messageRecord.getIndividualRecipient()); intent.putExtra("recipient", messageRecord.getIndividualRecipient());
intent.putExtra("body", messageRecord.getBody().getBody()); intent.putExtra("body", messageRecord.getBody().getBody());
intent.putExtra("thread_id", messageRecord.getThreadId()); intent.putExtra("thread_id", messageRecord.getThreadId());
intent.putExtra("message_id", messageRecord.getId());
intent.putExtra("master_secret", masterSecret); intent.putExtra("master_secret", masterSecret);
intent.putExtra("sent", messageRecord.isOutgoing()); intent.putExtra("sent", messageRecord.isOutgoing());
context.startActivity(intent); context.startActivity(intent);

View file

@ -21,10 +21,11 @@ public class DatabaseUpgradeActivity extends Activity {
public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 46; public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 46;
public static final int MMS_BODY_VERSION = 46; public static final int MMS_BODY_VERSION = 46;
public static final int TOFU_IDENTITIES_VERSION = 50;
private static final SortedSet<Integer> UPGRADE_VERSIONS = new TreeSet<Integer>() {{ private static final SortedSet<Integer> UPGRADE_VERSIONS = new TreeSet<Integer>() {{
add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION); add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION);
add(TOFU_IDENTITIES_VERSION);
}}; }};
private MasterSecret masterSecret; private MasterSecret masterSecret;

View file

@ -17,20 +17,32 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.content.Context; import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
/** /**
* List item view for displaying user identity keys. * List item view for displaying user identity keys.
* *
* @author Moxie Marlinspike * @author Moxie Marlinspike
*/ */
public class IdentityKeyView extends RelativeLayout { public class IdentityKeyView extends RelativeLayout
implements Recipient.RecipientModifiedListener
{
private TextView identityName; private TextView identityName;
private String identityKeyString;
private Recipients recipients;
private IdentityKey identityKey;
private final Handler handler = new Handler();
public IdentityKeyView(Context context) { public IdentityKeyView(Context context) {
super(context); super(context);
@ -45,17 +57,30 @@ public class IdentityKeyView extends RelativeLayout {
super(context, attributeSet); super(context, attributeSet);
} }
public void set(String name, String identityKeyString) { public void set(IdentityDatabase.Identity identity) {
identityName.setText(name); this.recipients = identity.getRecipients();
this.identityKeyString = identityKeyString; this.identityKey = identity.getIdentityKey();
this.recipients.addListener(this);
identityName.setText(recipients.toShortString());
} }
public String getIdentityKeyString() { public IdentityKey getIdentityKey() {
return this.identityKeyString; return this.identityKey;
} }
private void initializeResources() { private void initializeResources() {
this.identityName = (TextView)findViewById(R.id.identity_name); this.identityName = (TextView)findViewById(R.id.identity_name);
} }
@Override
public void onModified(Recipient recipient) {
handler.post(new Runnable() {
@Override
public void run() {
IdentityKeyView.this.identityName.setText(recipients.toShortString());
}
});
}
} }

View file

@ -1,40 +0,0 @@
package org.thoughtcrime.securesms;
import android.os.Bundle;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
public abstract class KeyVerifyingActivity extends KeyScanningActivity {
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
MenuInflater inflater = this.getSupportMenuInflater();
inflater.inflate(R.menu.verify_keys, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.menu_session_verified: handleVerified(); return true;
}
return false;
}
protected abstract void handleVerified();
}

View file

@ -16,14 +16,19 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.app.ProgressDialog;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException; import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.InvalidVersionException; import org.thoughtcrime.securesms.crypto.InvalidVersionException;
import org.thoughtcrime.securesms.crypto.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.KeyExchangeMessage;
@ -42,36 +47,32 @@ import org.thoughtcrime.securesms.util.MemoryCleaner;
public class ReceiveKeyActivity extends PassphraseRequiredSherlockActivity { public class ReceiveKeyActivity extends PassphraseRequiredSherlockActivity {
private TextView descriptionText; private TextView descriptionText;
private TextView signatureText;
private Button confirmButton; private Button confirmButton;
private Button cancelButton; private Button cancelButton;
private Button verifySessionButton;
private Button verifyIdentityButton;
private Recipient recipient; private Recipient recipient;
private long threadId; private long threadId;
private long messageId;
private MasterSecret masterSecret; private MasterSecret masterSecret;
private KeyExchangeMessage keyExchangeMessage; private KeyExchangeMessage keyExchangeMessage;
private KeyExchangeProcessor keyExchangeProcessor; private KeyExchangeProcessor keyExchangeProcessor;
private boolean sent;
@Override @Override
protected void onCreate(Bundle state) { protected void onCreate(Bundle state) {
super.onCreate(state); super.onCreate(state);
setContentView(R.layout.receive_key_activity); setContentView(R.layout.receive_key_activity);
initializeResources(); initializeResources();
try { try {
initializeKey(); initializeKey();
initializeText(); initializeText();
} catch (InvalidKeyException ike) { } catch (InvalidKeyException ike) {
Log.w("ReceiveKeyActivity", ike); Log.w("ReceiveKeyActivity", ike);
initializeCorruptedKeyText();
} catch (InvalidVersionException ive) { } catch (InvalidVersionException ive) {
initializeBadVersionText(); Log.w("ReceiveKeyActivity", ive);
} }
initializeListeners(); initializeListeners();
} }
@ -83,64 +84,20 @@ public class ReceiveKeyActivity extends PassphraseRequiredSherlockActivity {
} }
private void initializeText() { private void initializeText() {
if (keyExchangeProcessor.hasCompletedSession()) initializeTextForExistingSession(); SpannableString spannableString = new SpannableString(descriptionText.getText() + " " +
else initializeTextForNewSession(); getString(R.string.ReceiveKeyActivity_you_may_wish_to_verify_this_contact));
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
Intent intent = new Intent(ReceiveKeyActivity.this, VerifyIdentityActivity.class);
intent.putExtra("recipient", recipient);
intent.putExtra("master_secret", masterSecret);
startActivity(intent);
}
}, descriptionText.getText().length()+1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
initializeSignatureText(); descriptionText.setText(spannableString);
} descriptionText.setMovementMethod(LinkMovementMethod.getInstance());
private void initializeCorruptedKeyText() {
descriptionText.setText(R.string.ReceiveKeyActivity_error_you_have_received_a_corrupted_public_key);
confirmButton.setVisibility(View.GONE);
}
private void initializeBadVersionText() {
descriptionText.setText(R.string.ReceiveKeyActivity_error_you_have_received_a_public_key_from_an_unsupported_version_of_the_protocol);
confirmButton.setVisibility(View.GONE);
}
private void initializeSignatureText() {
if (!keyExchangeMessage.hasIdentityKey()) {
signatureText.setText(R.string.ReceiveKeyActivity_this_key_exchange_message_does_not_include_an_identity_signature);
return;
}
IdentityKey identityKey = keyExchangeMessage.getIdentityKey();
String identityName = DatabaseFactory.getIdentityDatabase(this).getNameForIdentity(masterSecret, identityKey);
if (identityName == null) {
signatureText.setText(R.string.ReceiveKeyActivity_this_key_exchange_message_includes_an_identity_signature_but_you_do_not_yet_trust_it);
} else {
signatureText.setText(String.format(getString(R.string.ReceiveKeyActivity_this_key_exchange_message_includes_an_identity_signature_which_you_trust_for_s), identityName));
}
}
private void initializeTextForExistingSession() {
if (keyExchangeProcessor.isRemoteKeyExchangeForExistingSession(keyExchangeMessage)) {
descriptionText.setText(String.format(getString(R.string.ReceiveKeyActivity_this_is_the_key_that_you_sent_to_start_your_current_encrypted_session_with_s), recipient.toShortString()));
this.confirmButton.setVisibility(View.GONE);
this.verifySessionButton.setVisibility(View.VISIBLE);
this.verifyIdentityButton.setVisibility(View.VISIBLE);
} else if (keyExchangeProcessor.isLocalKeyExchangeForExistingSession(keyExchangeMessage)) {
descriptionText.setText(String.format(getString(R.string.ReceiveKeyActivity_this_is_the_key_that_you_received_to_start_your_current_encrypted_session_with_s), recipient.toShortString()));
this.confirmButton.setVisibility(View.GONE);
this.verifySessionButton.setVisibility(View.VISIBLE);
this.verifyIdentityButton.setVisibility(View.VISIBLE);
} else {
descriptionText.setText(String.format(getString(R.string.ReceiveKeyActivity_you_have_received_a_key_exchange_message_from_s_warning_you_already_have_an_encrypted_session), recipient.toShortString()));
this.confirmButton.setVisibility(View.VISIBLE);
this.verifyIdentityButton.setVisibility(View.GONE);
this.verifySessionButton.setVisibility(View.GONE);
}
}
private void initializeTextForNewSession() {
if (keyExchangeProcessor.hasInitiatedSession() && !this.sent)
descriptionText.setText(String.format(getString(R.string.ReceiveKeyActivity_you_have_received_a_key_exchange_message_from_s_you_have_previously_initiated), recipient.toShortString()));
else if (keyExchangeProcessor.hasInitiatedSession() && this.sent)
descriptionText.setText(String.format(getString(R.string.ReceiveKeyActivity_you_have_initiated_a_key_exchange_message_with_s_but_have_not_yet_received_a_reply), recipient.toShortString()));
else if (!keyExchangeProcessor.hasInitiatedSession() && !this.sent)
descriptionText.setText(String.format(getString(R.string.ReceiveKeyActivity_you_have_received_a_key_exchange_message_from_s_you_have_no_existing_session), recipient.toShortString()));
} }
private void initializeKey() throws InvalidKeyException, InvalidVersionException { private void initializeKey() throws InvalidKeyException, InvalidVersionException {
@ -150,52 +107,46 @@ public class ReceiveKeyActivity extends PassphraseRequiredSherlockActivity {
private void initializeResources() { private void initializeResources() {
this.descriptionText = (TextView) findViewById(R.id.description_text); this.descriptionText = (TextView) findViewById(R.id.description_text);
this.signatureText = (TextView) findViewById(R.id.signature_text);
this.confirmButton = (Button) findViewById(R.id.ok_button); this.confirmButton = (Button) findViewById(R.id.ok_button);
this.cancelButton = (Button) findViewById(R.id.cancel_button); this.cancelButton = (Button) findViewById(R.id.cancel_button);
this.verifyIdentityButton = (Button) findViewById(R.id.verify_identity_button);
this.verifySessionButton = (Button) findViewById(R.id.verify_session_button);
this.recipient = getIntent().getParcelableExtra("recipient"); this.recipient = getIntent().getParcelableExtra("recipient");
this.threadId = getIntent().getLongExtra("thread_id", -1); this.threadId = getIntent().getLongExtra("thread_id", -1);
this.messageId = getIntent().getLongExtra("message_id", -1);
this.masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret"); this.masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret");
this.sent = getIntent().getBooleanExtra("sent", false);
this.keyExchangeProcessor = new KeyExchangeProcessor(this, masterSecret, recipient); this.keyExchangeProcessor = new KeyExchangeProcessor(this, masterSecret, recipient);
} }
private void initializeListeners() { private void initializeListeners() {
this.confirmButton.setOnClickListener(new OkListener()); this.confirmButton.setOnClickListener(new OkListener());
this.cancelButton.setOnClickListener(new CancelListener()); this.cancelButton.setOnClickListener(new CancelListener());
this.verifyIdentityButton.setOnClickListener(new VerifyIdentityListener());
this.verifySessionButton.setOnClickListener(new VerifySessionListener());
}
private class VerifyIdentityListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Intent intent = new Intent(ReceiveKeyActivity.this, VerifyIdentityActivity.class);
intent.putExtra("recipient", recipient);
intent.putExtra("master_secret", masterSecret);
startActivity(intent);
finish();
}
}
private class VerifySessionListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Intent intent = new Intent(ReceiveKeyActivity.this, VerifyKeysActivity.class);
intent.putExtra("recipient", recipient);
intent.putExtra("master_secret", masterSecret);
startActivity(intent);
finish();
}
} }
private class OkListener implements View.OnClickListener { private class OkListener implements View.OnClickListener {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
keyExchangeProcessor.processKeyExchangeMessage(keyExchangeMessage, threadId); new AsyncTask<Void, Void, Void> () {
finish(); private ProgressDialog dialog;
@Override
protected void onPreExecute() {
dialog = ProgressDialog.show(ReceiveKeyActivity.this, "Processing",
"Processing key exchange...", true);
}
@Override
protected Void doInBackground(Void... params) {
keyExchangeProcessor.processKeyExchangeMessage(keyExchangeMessage, threadId);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsProcessedKeyExchange(messageId);
return null;
}
@Override
protected void onPostExecute(Void result) {
dialog.dismiss();
finish();
}
}.execute();
} }
} }
@ -205,5 +156,4 @@ public class ReceiveKeyActivity extends PassphraseRequiredSherlockActivity {
ReceiveKeyActivity.this.finish(); ReceiveKeyActivity.this.finish();
} }
} }
} }

View file

@ -16,192 +16,24 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ListView;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockListActivity; import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem; import com.actionbarsherlock.view.MenuItem;
import org.thoughtcrime.securesms.crypto.IdentityKey; public class ReviewIdentitiesActivity extends SherlockFragmentActivity {
import org.thoughtcrime.securesms.crypto.InvalidKeyException; public void onCreate(Bundle bundle) {
import org.thoughtcrime.securesms.crypto.MasterCipher; super.onCreate(bundle);
import org.thoughtcrime.securesms.crypto.MasterSecret; setContentView(R.layout.review_identities);
import org.thoughtcrime.securesms.database.DatabaseFactory; getSupportActionBar().setDisplayHomeAsUpEnabled(true);
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import java.io.IOException;
/**
* Activity for reviewing/managing saved identity keys.
*
* @author Moxie Marlinspike
*/
public class ReviewIdentitiesActivity extends SherlockListActivity {
private static final int MENU_OPTION_DELETE = 2;
private MasterSecret masterSecret;
private MasterCipher masterCipher;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.review_identities);
this.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
initializeResources();
registerForContextMenu(this.getListView());
}
@Override
protected void onDestroy() {
masterCipher = null;
System.gc();
MemoryCleaner.clean(masterSecret);
super.onDestroy();
}
@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
viewIdentity(((IdentityKeyView)view).getIdentityKeyString());
}
@Override
public void onCreateContextMenu (ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
menu.add(0, MENU_OPTION_DELETE, Menu.NONE, R.string.delete);
}
@Override
public boolean onContextItemSelected(android.view.MenuItem item) {
Cursor cursor = ((CursorAdapter)this.getListAdapter()).getCursor();
String identityKeyString = cursor.getString(cursor.getColumnIndexOrThrow(IdentityDatabase.IDENTITY_KEY));
String identityName = cursor.getString(cursor.getColumnIndexOrThrow(IdentityDatabase.IDENTITY_NAME));
switch(item.getItemId()) {
case MENU_OPTION_DELETE:
deleteIdentity(identityName, identityKeyString);
return true;
}
return false;
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home: finish(); return true; case android.R.id.home: finish(); return true;
} }
return false; return false;
} }
}
private void initializeResources() {
this.masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret");
this.masterCipher = new MasterCipher(masterSecret);
Cursor cursor = DatabaseFactory.getIdentityDatabase(this).getIdentities();
this.startManagingCursor(cursor);
this.setListAdapter(new IdentitiesListAdapter(this, cursor));
}
private void viewIdentity(String identityKeyString) {
try {
Intent viewIntent = new Intent(this, ViewIdentityActivity.class);
viewIntent.putExtra("identity_key", new IdentityKey(Base64.decode(identityKeyString), 0));
startActivity(viewIntent);
} catch (InvalidKeyException ike) {
Log.w("ReviewIdentitiesActivity", ike);
Toast.makeText(this, R.string.ReviewIdentitiesActivity_unable_to_view_corrupted_identity_key_exclamation,
Toast.LENGTH_LONG).show();
} catch (IOException e) {
Log.w("ReviewIdentitiesActivity", e);
Toast.makeText(this, R.string.ReviewIdentitiesActivity_unable_to_view_corrupted_identity_key_exclamation,
Toast.LENGTH_LONG).show();
}
}
private void deleteIdentity(String name, String keyString) {
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setTitle(R.string.ReviewIdentitiesActivity_delete_identity);
alertDialog.setMessage(R.string.ReviewIdentitiesActivity_delete_identity_are_you_sure_you_want_to_delete_this_identity_key);
alertDialog.setCancelable(true);
alertDialog.setNegativeButton(R.string.no, null);
alertDialog.setPositiveButton(R.string.yes, new DeleteIdentityListener(name, keyString));
alertDialog.show();
}
private class DeleteIdentityListener implements OnClickListener {
private final String name;
private final String keyString;
public DeleteIdentityListener(String name, String keyString) {
this.name = name;
this.keyString = keyString;
}
public void onClick(DialogInterface arg0, int arg1) {
DatabaseFactory.getIdentityDatabase(ReviewIdentitiesActivity.this)
.deleteIdentity(name, keyString);
}
}
private class IdentitiesListAdapter extends CursorAdapter {
public IdentitiesListAdapter(Context context, Cursor cursor) {
super(context, cursor);
}
public IdentitiesListAdapter(Context context, Cursor c, boolean autoRequery) {
super(context, c, autoRequery);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
IdentityKey identityKey;
boolean valid;
String identityKeyString = cursor.getString(cursor.getColumnIndexOrThrow(IdentityDatabase.IDENTITY_KEY));
String identityName = cursor.getString(cursor.getColumnIndexOrThrow(IdentityDatabase.IDENTITY_NAME));
try {
String mac = cursor.getString(cursor.getColumnIndexOrThrow(IdentityDatabase.MAC));
valid = masterCipher.verifyMacFor(identityName + identityKeyString, Base64.decode(mac));
identityKey = new IdentityKey(Base64.decode(identityKeyString), 0);
} catch (InvalidKeyException ike) {
Log.w("ReviewIdentitiesActivity",ike);
valid = false;
} catch (IOException e) {
Log.w("ReviewIdentitiesActivity",e);
valid = false;
}
if (!valid)
identityName = getString(R.string.ReviewIdentitiesActivity_invalid_identity);
((IdentityKeyView)view).set(identityName, identityKeyString);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
IdentityKeyView identityKeyView = new IdentityKeyView(context);
bindView(identityKeyView, context, cursor);
return identityKeyView;
}
}
}

View file

@ -0,0 +1,90 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ListView;
import com.actionbarsherlock.app.SherlockListFragment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.loaders.IdentityLoader;
public class ReviewIdentitiesFragment extends SherlockListFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
private MasterSecret masterSecret;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
return inflater.inflate(R.layout.review_identities_fragment, container, false);
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
this.masterSecret = getSherlockActivity().getIntent().getParcelableExtra("master_secret");
initializeListAdapter();
getLoaderManager().initLoader(0, null, this);
}
@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
Intent viewIntent = new Intent(getActivity(), ViewIdentityActivity.class);
viewIntent.putExtra("identity_key", ((IdentityKeyView)view).getIdentityKey());
startActivity(viewIntent);
}
private void initializeListAdapter() {
this.setListAdapter(new IdentitiesListAdapter(getActivity(), null, masterSecret));
getLoaderManager().restartLoader(0, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new IdentityLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
((CursorAdapter)getListAdapter()).changeCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
((CursorAdapter)getListAdapter()).changeCursor(null);
}
private class IdentitiesListAdapter extends CursorAdapter {
private final MasterSecret masterSecret;
public IdentitiesListAdapter(Context context, Cursor cursor, MasterSecret masterSecret) {
super(context, cursor);
this.masterSecret = masterSecret;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
IdentityDatabase.Reader reader = DatabaseFactory.getIdentityDatabase(context)
.readerFor(masterSecret, cursor);
((IdentityKeyView)view).set(reader.getCurrent());
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
IdentityKeyView identityKeyView = new IdentityKeyView(context);
bindView(identityKeyView, context, cursor);
return identityKeyView;
}
}
}

View file

@ -1,123 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.MemoryCleaner;
/**
* Activity that provides interface for users to save
* identity keys they receive.
*
* @author Moxie Marlinspike
*/
public class SaveIdentityActivity extends PassphraseRequiredSherlockActivity {
private MasterSecret masterSecret;
private IdentityKey identityKey;
private EditText identityName;
private Button okButton;
private Button cancelButton;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.save_identity_activity);
initializeResources();
initializeListeners();
}
@Override
protected void onDestroy() {
MemoryCleaner.clean(masterSecret);
super.onDestroy();
}
private void initializeResources() {
String nameSuggestion = getIntent().getStringExtra("name_suggestion");
this.masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret");
this.identityKey = (IdentityKey)getIntent().getParcelableExtra("identity_key");
this.identityName = (EditText)findViewById(R.id.identity_name);
this.okButton = (Button)findViewById(R.id.ok_button);
this.cancelButton = (Button)findViewById(R.id.cancel_button);
if ((nameSuggestion != null) && (nameSuggestion.trim().length() > 0)) {
this.identityName.setText(nameSuggestion);
}
}
private void initializeListeners() {
this.okButton.setOnClickListener(new OkListener());
this.cancelButton.setOnClickListener(new CancelListener());
}
private class OkListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (identityName.getText() == null || identityName.getText().toString().trim().length() == 0) {
Toast.makeText(SaveIdentityActivity.this,
R.string.SaveIdentityActivity_you_must_specify_a_name_for_this_identity_exclamation,
Toast.LENGTH_LONG).show();
return;
}
try {
DatabaseFactory.getIdentityDatabase(SaveIdentityActivity.this).saveIdentity(masterSecret, identityKey, identityName.getText().toString());
} catch (InvalidKeyException e) {
AlertDialog.Builder builder = new AlertDialog.Builder(SaveIdentityActivity.this);
builder.setTitle(R.string.SaveIdentityActivity_identity_name_exists_exclamation);
builder.setMessage(R.string.SaveIdentityActivity_an_identity_key_with_the_specified_name_already_exists);
builder.setPositiveButton(R.string.SaveIdentityActivity_manage_identities,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(SaveIdentityActivity.this, ReviewIdentitiesActivity.class);
intent.putExtra("master_secret", masterSecret);
startActivity(intent);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
return;
}
finish();
}
}
private class CancelListener implements View.OnClickListener {
@Override
public void onClick(View v) {
finish();
}
}
}

View file

@ -16,13 +16,11 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.actionbarsherlock.view.MenuItem;
import org.thoughtcrime.securesms.crypto.IdentityKey; import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -35,7 +33,7 @@ import org.thoughtcrime.securesms.util.MemoryCleaner;
* *
* @author Moxie Marlinspike * @author Moxie Marlinspike
*/ */
public class VerifyIdentityActivity extends KeyVerifyingActivity { public class VerifyIdentityActivity extends KeyScanningActivity {
private Recipient recipient; private Recipient recipient;
private MasterSecret masterSecret; private MasterSecret masterSecret;
@ -46,6 +44,7 @@ public class VerifyIdentityActivity extends KeyVerifyingActivity {
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(state); super.onCreate(state);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.verify_identity_activity); setContentView(R.layout.verify_identity_activity);
initializeResources(); initializeResources();
@ -59,35 +58,12 @@ public class VerifyIdentityActivity extends KeyVerifyingActivity {
} }
@Override @Override
protected void handleVerified() { public boolean onOptionsItemSelected(MenuItem item) {
AlertDialog.Builder builder = new AlertDialog.Builder(this); switch (item.getItemId()) {
builder.setIcon(android.R.drawable.ic_dialog_alert); case android.R.id.home: finish(); return true;
builder.setTitle(R.string.VerifyIdentityActivity_mark_identity_verified_question); }
builder.setMessage(R.string.VerifyIdentityActivity_are_you_sure_you_have_validated_the_recipients_identity_fingerprint_and_would_like_to_mark_it_as_verified);
builder.setPositiveButton(R.string.VerifyIdentityActivity_mark_verified, return false;
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
SessionRecord sessionRecord = new SessionRecord(VerifyIdentityActivity.this,
masterSecret, recipient);
IdentityKey identityKey = sessionRecord.getIdentityKey();
String recipientName = recipient.getName();
Intent intent = new Intent(VerifyIdentityActivity.this,
SaveIdentityActivity.class);
intent.putExtra("name_suggestion", recipientName);
intent.putExtra("master_secret", masterSecret);
intent.putExtra("identity_key", identityKey);
startActivity(intent);
finish();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
} }
private void initializeLocalIdentityKey() { private void initializeLocalIdentityKey() {

View file

@ -1,180 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.MemoryCleaner;
/**
* Activity for verifying identities keys as they are imported.
*
* @author Moxie Marlinspike
*/
public class VerifyImportedIdentityActivity extends KeyScanningActivity {
private MasterSecret masterSecret;
private String contactName;
private IdentityKey identityKey;
private EditText identityName;
private TextView identityFingerprint;
private Button compareButton;
private Button verifiedButton;
private Button cancelButton;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.verify_imported_identity_activity);
initializeResources();
initializeFingerprints();
initializeListeners();
}
@Override
protected void onDestroy() {
MemoryCleaner.clean(masterSecret);
super.onDestroy();
}
private void initializeListeners() {
verifiedButton.setOnClickListener(new VerifiedButtonListener());
cancelButton.setOnClickListener(new CancelButtonListener());
compareButton.setOnClickListener(new CompareButtonListener());
}
private void initializeFingerprints() {
if (contactName != null)
identityName.setText(contactName);
identityFingerprint.setText(identityKey.getFingerprint());
}
private void initializeResources() {
masterSecret = (MasterSecret)this.getIntent().getParcelableExtra("master_secret");
identityFingerprint = (TextView)findViewById(R.id.imported_identity);
identityName = (EditText)findViewById(R.id.identity_name);
identityKey = (IdentityKey)this.getIntent().getParcelableExtra("identity_key");
contactName = (String)this.getIntent().getStringExtra("contact_name");
verifiedButton = (Button)findViewById(R.id.verified_button);
cancelButton = (Button)findViewById(R.id.cancel_button);
compareButton = (Button)findViewById(R.id.compare_button);
}
private class CancelButtonListener implements View.OnClickListener {
public void onClick(View v) {
finish();
}
}
private class CompareButtonListener implements View.OnClickListener {
public void onClick(View v) {
registerForContextMenu(compareButton);
compareButton.showContextMenu();
}
}
private class VerifiedButtonListener implements View.OnClickListener {
public void onClick(View v) {
if (identityName.getText() == null || identityName.getText().length() == 0) {
Toast.makeText(VerifyImportedIdentityActivity.this,
R.string.VerifyImportedIdentityActivity_you_must_specify_a_name_for_this_contact_exclamation,
Toast.LENGTH_LONG);
return;
}
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(VerifyImportedIdentityActivity.this);
dialogBuilder.setTitle(R.string.VerifyImportedIdentityActivity_save_identity_key_question);
dialogBuilder.setIcon(android.R.drawable.ic_dialog_info);
dialogBuilder.setMessage(String.format(getString(R.string.VerifyImportedIdentityActivity_are_you_sure_that_you_would_like_to_mark_this_as_a_valid_identity_key_for_all_future_correspondence_with_s), identityName.getText()));
dialogBuilder.setCancelable(true);
dialogBuilder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
try {
DatabaseFactory.getIdentityDatabase(VerifyImportedIdentityActivity.this).saveIdentity(masterSecret, identityKey, identityName.getText().toString());
} catch (InvalidKeyException ike) {
Log.w("VerifiedButtonListener", ike);
Dialogs.displayAlert(VerifyImportedIdentityActivity.this,
getString(R.string.VerifyImportedIdentityActivity_error_saving_identity_key_exclamation),
getString(R.string.VerifyImportedIdentityActivity_this_identity_key_or_an_identity_key_with_the_same_name_already_exists_please_edit_your_key_database),
android.R.drawable.ic_dialog_alert);
return;
}
finish();
}
});
dialogBuilder.setNegativeButton(R.string.no, null);
dialogBuilder.show();
}
}
@Override
protected String getScanString() {
return getString(R.string.VerifyImportedIdentityActivity_scan_to_compare);
}
@Override
protected String getDisplayString() {
return getString(R.string.VerifyImportedIdentityActivity_get_scanned_to_compare);
}
@Override
protected IdentityKey getIdentityKeyToCompare() {
return identityKey;
}
@Override
protected IdentityKey getIdentityKeyToDisplay() {
return identityKey;
}
@Override
protected String getNotVerifiedMessage() {
return getString(R.string.VerifyImportedIdentityActivity_warning_the_scanned_key_does_not_match_exclamation);
}
@Override
protected String getNotVerifiedTitle() {
return getString(R.string.VerifyImportedIdentityActivity_not_verified_exclamation);
}
@Override
protected String getVerifiedMessage() {
return getString(R.string.VerifyImportedIdentityActivity_the_scanned_key_matches_exclamation);
}
@Override
protected String getVerifiedTitle() {
return getString(R.string.VerifyImportedIdentityActivity_verified_exclamation);
}
}

View file

@ -16,8 +16,6 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import android.widget.TextView; import android.widget.TextView;
@ -34,7 +32,7 @@ import org.thoughtcrime.securesms.util.MemoryCleaner;
* @author Moxie Marlinspike * @author Moxie Marlinspike
* *
*/ */
public class VerifyKeysActivity extends KeyVerifyingActivity { public class VerifyKeysActivity extends KeyScanningActivity {
private byte[] yourFingerprintBytes; private byte[] yourFingerprintBytes;
private byte[] theirFingerprintBytes; private byte[] theirFingerprintBytes;
@ -60,28 +58,6 @@ public class VerifyKeysActivity extends KeyVerifyingActivity {
super.onDestroy(); super.onDestroy();
} }
@Override
protected void handleVerified() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(R.string.VerifyKeysActivity_mark_session_verified_question);
builder.setMessage(R.string.VerifyKeysActivity_are_you_sure_that_you_have_validated_these_fingerprints_and_would_like_to_mark_this_session_as_verified);
builder.setPositiveButton(R.string.VerifyKeysActivity_mark_verified,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
SessionRecord sessionRecord = new SessionRecord(VerifyKeysActivity.this, masterSecret,
recipient);
sessionRecord.setVerifiedSessionKey(true);
sessionRecord.save();
VerifyKeysActivity.this.finish();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
private void initializeResources() { private void initializeResources() {
this.recipient = (Recipient)this.getIntent().getParcelableExtra("recipient"); this.recipient = (Recipient)this.getIntent().getParcelableExtra("recipient");
this.masterSecret = (MasterSecret)this.getIntent().getParcelableExtra("master_secret"); this.masterSecret = (MasterSecret)this.getIntent().getParcelableExtra("master_secret");

View file

@ -1,62 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.keys.SessionRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
public class AuthenticityCalculator {
private static boolean isAuthenticatedIdentity(Context context,
MasterSecret masterSecret,
IdentityKey identityKey)
{
String identityName = DatabaseFactory.getIdentityDatabase(context)
.getNameForIdentity(masterSecret, identityKey);
if (identityName == null) return false;
else return true;
}
public static String getAuthenticatedName(Context context,
Recipient recipient,
MasterSecret masterSecret)
{
SessionRecord session = new SessionRecord(context, masterSecret, recipient);
return DatabaseFactory.getIdentityDatabase(context)
.getNameForIdentity(masterSecret, session.getIdentityKey());
}
public static boolean isAuthenticated(Context context,
Recipient recipient,
MasterSecret masterSecret)
{
SessionRecord session = new SessionRecord(context, masterSecret, recipient);
if (session.isVerifiedSession()) {
return true;
} else if (session.getIdentityKey() != null) {
return isAuthenticatedIdentity(context, masterSecret, session.getIdentityKey());
}
return false;
}
}

View file

@ -307,9 +307,7 @@ public class DecryptingQueue {
if (processor.isStale(keyExchangeMessage)) { if (processor.isStale(keyExchangeMessage)) {
DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId); DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId);
} else if (!processor.hasCompletedSession() || } else if (processor.isTrusted(keyExchangeMessage)) {
processor.hasSameSessionIdentity(keyExchangeMessage))
{
DatabaseFactory.getEncryptingSmsDatabase(context).markAsProcessedKeyExchange(messageId); DatabaseFactory.getEncryptingSmsDatabase(context).markAsProcessedKeyExchange(messageId);
processor.processKeyExchangeMessage(keyExchangeMessage, threadId); processor.processKeyExchangeMessage(keyExchangeMessage, threadId);
} }

View file

@ -21,6 +21,7 @@ import android.content.Intent;
import android.util.Log; import android.util.Log;
import org.bouncycastle.util.Arrays; import org.bouncycastle.util.Arrays;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.keys.LocalKeyRecord; import org.thoughtcrime.securesms.database.keys.LocalKeyRecord;
import org.thoughtcrime.securesms.database.keys.RemoteKeyRecord; import org.thoughtcrime.securesms.database.keys.RemoteKeyRecord;
import org.thoughtcrime.securesms.database.keys.SessionRecord; import org.thoughtcrime.securesms.database.keys.SessionRecord;
@ -58,16 +59,13 @@ public class KeyExchangeProcessor {
this.sessionRecord = new SessionRecord(context, masterSecret, recipient); this.sessionRecord = new SessionRecord(context, masterSecret, recipient);
} }
public boolean hasCompletedSession() { public boolean isTrusted(KeyExchangeMessage message) {
return sessionRecord.getLocalFingerprint() != null; if (!message.hasIdentityKey()) {
} return false;
}
public boolean hasSameSessionIdentity(KeyExchangeMessage message) { return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, recipient,
return message.getIdentityKey());
(this.sessionRecord.getIdentityKey() != null) &&
(message.getIdentityKey() != null) &&
(this.sessionRecord.getIdentityKey().equals(message.getIdentityKey()) &&
!isRemoteKeyExchangeForExistingSession(message));
} }
public boolean hasInitiatedSession() { public boolean hasInitiatedSession() {
@ -78,14 +76,6 @@ public class KeyExchangeProcessor {
return !hasInitiatedSession() || remoteKeyRecord.getCurrentRemoteKey() != null; return !hasInitiatedSession() || remoteKeyRecord.getCurrentRemoteKey() != null;
} }
public boolean isRemoteKeyExchangeForExistingSession(KeyExchangeMessage message) {
return Arrays.areEqual(message.getPublicKey().getFingerprintBytes(), sessionRecord.getRemoteFingerprint());
}
public boolean isLocalKeyExchangeForExistingSession(KeyExchangeMessage message) {
return Arrays.areEqual(message.getPublicKey().getFingerprintBytes(), sessionRecord.getLocalFingerprint());
}
public boolean isStale(KeyExchangeMessage message) { public boolean isStale(KeyExchangeMessage message) {
int responseKeyId = Conversions.highBitsToMedium(message.getPublicKey().getId()); int responseKeyId = Conversions.highBitsToMedium(message.getPublicKey().getId());
@ -121,6 +111,9 @@ public class KeyExchangeProcessor {
sessionRecord.save(); sessionRecord.save();
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, message.getIdentityKey());
DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient); DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient);
Intent intent = new Intent(SECURITY_UPDATE_EVENT); Intent intent = new Intent(SECURITY_UPDATE_EVENT);

View file

@ -91,6 +91,14 @@ public class KeyUtil {
(RemoteKeyRecord.hasRecord(context, recipient)) && (RemoteKeyRecord.hasRecord(context, recipient)) &&
(SessionRecord.hasSession(context, recipient)); (SessionRecord.hasSession(context, recipient));
} }
public static boolean isIdentityKeyFor(Context context,
MasterSecret masterSecret,
Recipient recipient)
{
return isSessionFor(context, recipient) &&
new SessionRecord(context, masterSecret, recipient).getIdentityKey() != null;
}
public static LocalKeyRecord initializeRecordFor(Recipient recipient, Context context, MasterSecret masterSecret) { public static LocalKeyRecord initializeRecordFor(Recipient recipient, Context context, MasterSecret masterSecret) {
Log.w("KeyUtil", "Initializing local key pairs..."); Log.w("KeyUtil", "Initializing local key pairs...");

View file

@ -26,9 +26,12 @@ import android.util.Log;
import org.thoughtcrime.securesms.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.keys.SessionRecord;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.InvalidMessageException; import org.thoughtcrime.securesms.util.InvalidMessageException;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -40,14 +43,15 @@ import ws.com.google.android.mms.ContentType;
public class DatabaseFactory { public class DatabaseFactory {
private static final int INTRODUCED_IDENTITIES_VERSION = 2; private static final int INTRODUCED_IDENTITIES_VERSION = 2;
private static final int INTRODUCED_INDEXES_VERSION = 3; private static final int INTRODUCED_INDEXES_VERSION = 3;
private static final int INTRODUCED_DATE_SENT_VERSION = 4; private static final int INTRODUCED_DATE_SENT_VERSION = 4;
private static final int INTRODUCED_DRAFTS_VERSION = 5; private static final int INTRODUCED_DRAFTS_VERSION = 5;
private static final int INTRODUCED_NEW_TYPES_VERSION = 6; private static final int INTRODUCED_NEW_TYPES_VERSION = 6;
private static final int INTRODUCED_MMS_BODY_VERSION = 7; private static final int INTRODUCED_MMS_BODY_VERSION = 7;
private static final int INTRODUCED_MMS_FROM_VERSION = 8; private static final int INTRODUCED_MMS_FROM_VERSION = 8;
private static final int DATABASE_VERSION = 8; private static final int INTRODUCED_TOFU_IDENTITY_VERSION = 9;
private static final int DATABASE_VERSION = 9;
private static final String DATABASE_NAME = "messages.db"; private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object(); private static final Object lock = new Object();
@ -357,6 +361,36 @@ public class DatabaseFactory {
} }
} }
if (fromVersion < DatabaseUpgradeActivity.TOFU_IDENTITIES_VERSION) {
File sessionDirectory = new File(context.getFilesDir() + File.separator + "sessions");
if (sessionDirectory.exists() && sessionDirectory.isDirectory()) {
File[] sessions = sessionDirectory.listFiles();
if (sessions != null) {
for (File session : sessions) {
String name = session.getName();
if (name.matches("[0-9]+")) {
long recipientId = Long.parseLong(name);
SessionRecord sessionRecord = new SessionRecord(context, masterSecret, recipientId);
IdentityKey identityKey = sessionRecord.getIdentityKey();
if (identityKey != null) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
String identityKeyString = Base64.encodeBytes(identityKey.serialize());
String macString = Base64.encodeBytes(masterCipher.getMacFor(recipientId +
identityKeyString));
db.execSQL("REPLACE INTO identities (recipient, key, mac) VALUES (?, ?, ?)",
new String[] {recipientId+"", identityKeyString, macString});
}
}
}
}
}
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
@ -566,6 +600,11 @@ public class DatabaseFactory {
cursor.close(); cursor.close();
} }
if (oldVersion < INTRODUCED_TOFU_IDENTITY_VERSION) {
db.execSQL("DROP TABLE identities");
db.execSQL("CREATE TABLE identities (_id INTEGER PRIMARY KEY, recipient INTEGER UNIQUE, key TEXT, mac TEXT);");
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
} }

View file

@ -28,6 +28,9 @@ import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException; import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import java.io.IOException; import java.io.IOException;
@ -38,13 +41,15 @@ public class IdentityDatabase extends Database {
private static final String TABLE_NAME = "identities"; private static final String TABLE_NAME = "identities";
private static final String ID = "_id"; private static final String ID = "_id";
public static final String RECIPIENT = "recipient";
public static final String IDENTITY_KEY = "key"; public static final String IDENTITY_KEY = "key";
public static final String IDENTITY_NAME = "name";
public static final String MAC = "mac"; public static final String MAC = "mac";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
IDENTITY_KEY + " TEXT UNIQUE, " + IDENTITY_NAME + " TEXT UNIQUE, " + " (" + ID + " INTEGER PRIMARY KEY, " +
MAC + " TEXT);"; RECIPIENT + " INTEGER UNIQUE, " +
IDENTITY_KEY + " TEXT, " +
MAC + " TEXT);";
public IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) { public IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
@ -60,69 +65,127 @@ public class IdentityDatabase extends Database {
return cursor; return cursor;
} }
public String getNameForIdentity(MasterSecret masterSecret, IdentityKey identityKey) { public boolean isValidIdentity(MasterSecret masterSecret,
if (identityKey == null) Recipient recipient,
return null; IdentityKey theirIdentity)
{
MasterCipher masterCipher = new MasterCipher(masterSecret);
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
String number = recipient.getNumber();
long recipientId = DatabaseFactory.getAddressDatabase(context).getCanonicalAddress(number);
MasterCipher masterCipher = new MasterCipher(masterSecret);
Cursor cursor = null; Cursor cursor = null;
Log.w("IdentityDatabase", "Querying for: " + Base64.encodeBytes(identityKey.serialize()));
try { try {
cursor = database.query(TABLE_NAME, null, IDENTITY_KEY + " = ?", new String[] {Base64.encodeBytes(identityKey.serialize())}, null, null, null); cursor = database.query(TABLE_NAME, null, RECIPIENT + " = ?",
new String[] {recipientId+""}, null, null,null);
if (cursor == null || !cursor.moveToFirst()) if (cursor != null && cursor.moveToFirst()) {
return null; String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
String mac = cursor.getString(cursor.getColumnIndexOrThrow(MAC));
String identityName = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_NAME)); if (!masterCipher.verifyMacFor(recipientId + serializedIdentity, Base64.decode(mac))) {
String identityKeyString = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY)); Log.w("IdentityDatabase", "MAC failed");
byte[] mac = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(MAC))); return false;
}
if (!masterCipher.verifyMacFor(identityName + identityKeyString, mac)) { IdentityKey ourIdentity = new IdentityKey(Base64.decode(serializedIdentity), 0);
Log.w("IdentityDatabase", "Mac check failed!"); return ourIdentity.equals(theirIdentity);
return null; } else {
return true;
} }
Log.w("IdentityDatabase", "Returning identity name: " + identityName);
return identityName;
} catch (IOException e) { } catch (IOException e) {
Log.w("IdentityDatabase", e); Log.w("IdentityDatabase", e);
return null; return false;
} catch (InvalidKeyException e) {
Log.w("IdentityDatabase", e);
return false;
} finally { } finally {
if (cursor != null) if (cursor != null) {
cursor.close(); cursor.close();
}
} }
} }
public void saveIdentity(MasterSecret masterSecret, IdentityKey identityKey, String tagName) throws InvalidKeyException { public void saveIdentity(MasterSecret masterSecret, Recipient recipient, IdentityKey identityKey)
{
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
String number = recipient.getNumber();
long recipientId = DatabaseFactory.getAddressDatabase(context).getCanonicalAddress(number);
MasterCipher masterCipher = new MasterCipher(masterSecret); MasterCipher masterCipher = new MasterCipher(masterSecret);
String identityKeyString = Base64.encodeBytes(identityKey.serialize()); String identityKeyString = Base64.encodeBytes(identityKey.serialize());
String macString = Base64.encodeBytes(masterCipher.getMacFor(tagName + identityKeyString)); String macString = Base64.encodeBytes(masterCipher.getMacFor(recipientId +
identityKeyString));
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(RECIPIENT, recipientId);
contentValues.put(IDENTITY_KEY, identityKeyString); contentValues.put(IDENTITY_KEY, identityKeyString);
contentValues.put(IDENTITY_NAME, tagName);
contentValues.put(MAC, macString); contentValues.put(MAC, macString);
long id = database.insert(TABLE_NAME, null, contentValues); database.replace(TABLE_NAME, null, contentValues);
if (id == -1)
throw new InvalidKeyException("Error inserting key!");
context.getContentResolver().notifyChange(CHANGE_URI, null); context.getContentResolver().notifyChange(CHANGE_URI, null);
} }
public void deleteIdentity(String name, String keyString) { public void deleteIdentity(long id) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
String where = IDENTITY_NAME + " = ? AND " + IDENTITY_KEY + " = ?"; database.delete(TABLE_NAME, ID_WHERE, new String[] {id+""});
String[] args = new String[] {name, keyString};
database.delete(TABLE_NAME, where, args);
context.getContentResolver().notifyChange(CHANGE_URI, null); context.getContentResolver().notifyChange(CHANGE_URI, null);
} }
public Reader readerFor(MasterSecret masterSecret, Cursor cursor) {
return new Reader(masterSecret, cursor);
}
public class Reader {
private final Cursor cursor;
private final MasterCipher cipher;
public Reader(MasterSecret masterSecret, Cursor cursor) {
this.cursor = cursor;
this.cipher = new MasterCipher(masterSecret);
}
public Identity getCurrent() {
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT));
Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientId + "", true);
try {
String identityKeyString = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
String mac = cursor.getString(cursor.getColumnIndexOrThrow(MAC));
if (!cipher.verifyMacFor(recipientId + identityKeyString, Base64.decode(mac))) {
return new Identity(recipients, null);
}
IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyString), 0);
return new Identity(recipients, identityKey);
} catch (IOException e) {
Log.w("IdentityDatabase", e);
return new Identity(recipients, null);
} catch (InvalidKeyException e) {
Log.w("IdentityDatabase", e);
return new Identity(recipients, null);
}
}
}
public class Identity {
private final Recipients recipients;
private final IdentityKey identityKey;
public Identity(Recipients recipients, IdentityKey identityKey) {
this.recipients = recipients;
this.identityKey = identityKey;
}
public Recipients getRecipients() {
return recipients;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
}
} }

View file

@ -23,8 +23,6 @@ import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException; import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.CanonicalAddressDatabase; import org.thoughtcrime.securesms.database.CanonicalAddressDatabase;
import org.thoughtcrime.securesms.database.keys.Record;
import org.thoughtcrime.securesms.database.keys.SessionKey;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -56,23 +54,27 @@ public class SessionRecord extends Record {
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
public SessionRecord(Context context, MasterSecret masterSecret, Recipient recipient) { public SessionRecord(Context context, MasterSecret masterSecret, Recipient recipient) {
super(context, getFileNameForRecipient(context, recipient)); this(context, masterSecret, getRecipientId(context, recipient));
}
public SessionRecord(Context context, MasterSecret masterSecret, long recipientId) {
super(context, recipientId+"");
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
this.sessionVersion = 31337; this.sessionVersion = 31337;
loadData(); loadData();
} }
public static void delete(Context context, Recipient recipient) { public static void delete(Context context, Recipient recipient) {
Record.delete(context, getFileNameForRecipient(context, recipient)); Record.delete(context, getRecipientId(context, recipient)+"");
} }
public static boolean hasSession(Context context, Recipient recipient) { public static boolean hasSession(Context context, Recipient recipient) {
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient)); Log.w("LocalKeyRecord", "Checking: " + getRecipientId(context, recipient));
return Record.hasRecord(context, getFileNameForRecipient(context, recipient)); return Record.hasRecord(context, getRecipientId(context, recipient)+"");
} }
private static String getFileNameForRecipient(Context context, Recipient recipient) { private static long getRecipientId(Context context, Recipient recipient) {
return CanonicalAddressDatabase.getInstance(context).getCanonicalAddress(recipient.getNumber()) + ""; return CanonicalAddressDatabase.getInstance(context).getCanonicalAddress(recipient.getNumber());
} }
public void setSessionKey(SessionKey sessionKeyRecord) { public void setSessionKey(SessionKey sessionKeyRecord) {
@ -116,9 +118,9 @@ public class SessionRecord extends Record {
return this.identityKey; return this.identityKey;
} }
public void setVerifiedSessionKey(boolean verifiedSessionKey) { // public void setVerifiedSessionKey(boolean verifiedSessionKey) {
this.verifiedSessionKey = verifiedSessionKey; // this.verifiedSessionKey = verifiedSessionKey;
} // }
public boolean isVerifiedSession() { public boolean isVerifiedSession() {
return this.verifiedSessionKey; return this.verifiedSessionKey;
@ -130,8 +132,8 @@ public class SessionRecord extends Record {
} }
private boolean isValidVersionMarker(int versionMarker) { private boolean isValidVersionMarker(int versionMarker) {
for (int i=0;i<VALID_VERSION_MARKERS.length;i++) for (int VALID_VERSION_MARKER : VALID_VERSION_MARKERS)
if (versionMarker == VALID_VERSION_MARKERS[i]) if (versionMarker == VALID_VERSION_MARKER)
return true; return true;
return false; return false;
@ -199,7 +201,7 @@ public class SessionRecord extends Record {
if (versionMarker >= 0X55555556) { if (versionMarker >= 0X55555556) {
readIdentityKey(in); readIdentityKey(in);
this.verifiedSessionKey = (readInteger(in) == 1) ? true : false; this.verifiedSessionKey = (readInteger(in) == 1);
} }
if (in.available() != 0) if (in.available() != 0)
@ -209,7 +211,7 @@ public class SessionRecord extends Record {
} }
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.w("SessionRecord", "No session information found."); Log.w("SessionRecord", "No session information found.");
return; // XXX
} catch (IOException ioe) { } catch (IOException ioe) {
Log.w("keyrecord", ioe); Log.w("keyrecord", ioe);
// XXX // XXX

View file

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.database.loaders;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.CursorLoader;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
public class IdentityLoader extends CursorLoader {
private final Context context;
public IdentityLoader(Context context) {
super(context);
this.context = context.getApplicationContext();
}
@Override
public Cursor loadInBackground() {
return DatabaseFactory.getIdentityDatabase(context).getIdentities();
}
}

View file

@ -108,7 +108,7 @@ public class SmsReceiver {
if (processor.isStale(keyExchangeMessage)) { if (processor.isStale(keyExchangeMessage)) {
message.setStale(true); message.setStale(true);
} else if (!processor.hasCompletedSession() || processor.hasSameSessionIdentity(keyExchangeMessage)) { } else if (processor.isTrusted(keyExchangeMessage)) {
message.setProcessed(true); message.setProcessed(true);
Pair<Long, Long> messageAndThreadId = storeStandardMessage(masterSecret, message); Pair<Long, Long> messageAndThreadId = storeStandardMessage(masterSecret, message);