diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index cab5c80b94ca3db32355596785e7ee1a249a4138..cb723afa40e7011d2ce44d7eb0810328c5093bc1 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -14,11 +14,13 @@ /> <uses-feature android:name="android.hardware.bluetooth"/> + <uses-feature android:name="android.hardware.camera" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> + <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_LOGS"/> <uses-permission android:name="android.permission.VIBRATE" /> @@ -165,6 +167,16 @@ android:value=".android.NavDrawerActivity" /> </activity> + <activity + android:name=".android.keyagreement.KeyAgreementActivity" + android:label="@string/add_contact_title" + android:theme="@style/BriarThemeNoActionBar.Default" + android:parentActivityName=".android.NavDrawerActivity"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".android.NavDrawerActivity" + /> + </activity> <activity android:name=".android.StartupFailureActivity" android:label="@string/startup_failed_activity_title"> diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 81228e91863e36d784813798f1c84c57615a3254..7e79c0dffb8beccb996572f080d61001d71f22e5 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -46,7 +46,7 @@ dependencyVerification { 'com.android.support:recyclerview-v7:7606373da0931a1e62588335465a0e390cd676c98117edab29220317495faefd', 'info.guardianproject.panic:panic:a7ed9439826db2e9901649892cf9afbe76f00991b768d8f4c26332d7c9406cb2', 'info.guardianproject.trustedintents:trustedintents:6221456d8821a8d974c2acf86306900237cf6afaaa94a4c9c44e161350f80f3e', - 'com.android.support:support-annotations:f347a35b9748a4103b39a6714a77e2100f488d623fd6268e259c177b200e9d82' + 'de.hdodenhof:circleimageview:c76d936395b50705a3f98c9220c22d2599aeb9e609f559f6048975cfc1f686b8', ] } @@ -97,4 +97,4 @@ android { lintOptions { abortOnError false } -} \ No newline at end of file +} diff --git a/briar-android/res/layout/activity_nav_drawer.xml b/briar-android/res/layout/activity_nav_drawer.xml index f7f90efa8e99a2be3cbf94dc1dc30995d20cc566..5b167e4ffe3951eb5ed034676dadef175fa7476f 100644 --- a/briar-android/res/layout/activity_nav_drawer.xml +++ b/briar-android/res/layout/activity_nav_drawer.xml @@ -2,65 +2,12 @@ <android.support.v4.widget.DrawerLayout android:id="@+id/drawer_layout" xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- The first child(root) is the content view --> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <android.support.v7.widget.Toolbar - android:id="@+id/toolbar" - style="@style/BriarToolbar" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?attr/colorPrimary" - android:minHeight="?attr/actionBarSize" - /> - - <FrameLayout - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <FrameLayout - android:id="@+id/content_fragment" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@color/default_background"/> - - <RelativeLayout - android:id="@+id/container_progress" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:background="@color/default_background" - android:visibility="invisible" - tools:visibility="visible"> - - <ProgressBar - android:id="@+id/progress_bar" - style="?android:attr/progressBarStyleLargeInverse" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerInParent="true"/> - - <TextView - android:id="@+id/title_progress_bar" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/progress_bar" - android:gravity="center" - android:paddingTop="@dimen/margin_large" - tools:text="progress bar title" - /> - - </RelativeLayout> - - </FrameLayout> - </LinearLayout> + <include + layout="@layout/activity_with_loading"/> <!-- The second child is the menu --> <include diff --git a/briar-android/res/layout/activity_with_loading.xml b/briar-android/res/layout/activity_with_loading.xml new file mode 100644 index 0000000000000000000000000000000000000000..8d22ba321a5a950e1df4265cbeb0e98c7df25615 --- /dev/null +++ b/briar-android/res/layout/activity_with_loading.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <android.support.v7.widget.Toolbar + android:id="@+id/toolbar" + style="@style/BriarToolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorPrimary" + android:minHeight="?attr/actionBarSize" + /> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:id="@+id/content_fragment" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/default_background"/> + + <RelativeLayout + android:id="@+id/container_progress" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:background="@color/default_background" + android:visibility="invisible" + tools:visibility="visible"> + + <ProgressBar + android:id="@+id/progress_bar" + style="?android:attr/progressBarStyleLargeInverse" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true"/> + + <TextView + android:id="@+id/title_progress_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/progress_bar" + android:gravity="center" + android:paddingTop="@dimen/margin_large" + tools:text="progress bar title" + /> + + </RelativeLayout> + + </FrameLayout> +</LinearLayout> \ No newline at end of file diff --git a/briar-android/res/layout/fragment_keyagreement_qr.xml b/briar-android/res/layout/fragment_keyagreement_qr.xml new file mode 100644 index 0000000000000000000000000000000000000000..31f15925b70a355c35019d28c9cd81b35d81be5d --- /dev/null +++ b/briar-android/res/layout/fragment_keyagreement_qr.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + android:id="@+id/qr_layout" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:weightSum="2"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:background="@android:color/black" + android:gravity="center"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/background_light"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:gravity="center" + android:orientation="vertical" + android:padding="@dimen/margin_medium"> + + <ProgressBar + style="?android:attr/progressBarStyleInverse" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingTop="@dimen/margin_large"/> + + <TextView + android:id="@+id/connect_status" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:paddingTop="@dimen/margin_large" + tools:text="Connection failed"/> + </LinearLayout> + </RelativeLayout> + + <org.briarproject.android.util.CameraView + android:id="@+id/camera_view" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + </FrameLayout> + + <ImageView + android:id="@+id/qr_code" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:scaleType="fitCenter"/> +</LinearLayout> \ No newline at end of file diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index cf63cf76bbcfb443dd4c21583f4121cfd6685ed5..f75d8a90d2a549e18c3bbd8fc1bd0938867c8f70 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -34,6 +34,7 @@ <string name="contact_list_title">Contacts</string> <string name="no_contacts">No contacts</string> <string name="add_contact_title">Add a Contact</string> + <string name="add_contact_title_step">Add a Contact - Step %1$d/%2$d</string> <string name="your_nickname">Choose the identity you want to use:</string> <string name="face_to_face">You must be face-to-face with the person you want to add as a contact. This will prevent anyone from impersonating you or reading your messages in future.</string> <string name="continue_button">Continue</string> @@ -52,6 +53,14 @@ <string name="codes_do_not_match">Codes do not match</string> <string name="interfering">This could mean that someone is trying to interfere with your connection</string> <string name="contact_added_toast">Contact added: %s</string> + <string name="contact_already_exists">Contact %s already exists</string> + <string name="contact_exchange_failed">Contact exchange failed</string> + <string name="scan_qr_code">Scan QR code</string> + <string name="qr_code_invalid">The QR code is invalid</string> + <string name="connecting_to_device">Connecting to device\u2026</string> + <string name="authenticating_with_device">Authenticating with device\u2026</string> + <string name="connection_aborted_local">Connection aborted by us! This could mean that someone is trying to interfere with your connection</string> + <string name="connection_aborted_remote">Connection aborted by your contact! This could mean that someone is trying to interfere with your connection</string> <string name="no_private_messages">No messages</string> <string name="private_message_hint">Type message</string> <string name="message_sent_toast">Message sent</string> diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java index 4db188d9f60f2fa9618ec5333aada3a0c4f44542..a00d6fb1293179d1ed9f9282416f6f7e8e7494b2 100644 --- a/briar-android/src/org/briarproject/android/AndroidComponent.java +++ b/briar-android/src/org/briarproject/android/AndroidComponent.java @@ -13,6 +13,9 @@ import org.briarproject.android.forum.ShareForumActivity; import org.briarproject.android.forum.WriteForumPostActivity; import org.briarproject.android.identity.CreateIdentityActivity; import org.briarproject.android.invitation.AddContactActivity; +import org.briarproject.android.keyagreement.ChooseIdentityFragment; +import org.briarproject.android.keyagreement.KeyAgreementActivity; +import org.briarproject.android.keyagreement.ShowQrCodeFragment; import org.briarproject.android.panic.PanicPreferencesActivity; import org.briarproject.android.panic.PanicResponderActivity; import org.briarproject.plugins.AndroidPluginsModule; @@ -44,6 +47,8 @@ public interface AndroidComponent extends CoreEagerSingletons { void inject(AddContactActivity activity); + void inject(KeyAgreementActivity activity); + void inject(ConversationActivity activity); void inject(CreateIdentityActivity activity); @@ -68,6 +73,10 @@ public interface AndroidComponent extends CoreEagerSingletons { void inject(ForumListFragment fragment); + void inject(ChooseIdentityFragment fragment); + + void inject(ShowQrCodeFragment fragment); + // Eager singleton load void inject(AndroidModule.EagerSingletons init); diff --git a/briar-android/src/org/briarproject/android/BriarFragmentActivity.java b/briar-android/src/org/briarproject/android/BriarFragmentActivity.java index 7b1e6ff535aa49cca6fc6778d06bd705daf62229..6bef438cf98803912918baaee7c8ab2ea79e916a 100644 --- a/briar-android/src/org/briarproject/android/BriarFragmentActivity.java +++ b/briar-android/src/org/briarproject/android/BriarFragmentActivity.java @@ -42,7 +42,8 @@ public abstract class BriarFragmentActivity extends BriarActivity { @Override public void onBackPressed() { - if (getSupportFragmentManager().getBackStackEntryCount() == 0 && + if (this instanceof NavDrawerActivity && + getSupportFragmentManager().getBackStackEntryCount() == 0 && getSupportFragmentManager() .findFragmentByTag(ContactListFragment.TAG) == null) { /* diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java index 30d7ecb5d9ddc0d783da2bbd958c26330c90e574..0d7c5d32356547ca4c661554b99c46866979422b 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java @@ -13,7 +13,7 @@ import org.briarproject.R; import org.briarproject.android.AndroidComponent; import org.briarproject.android.BriarApplication; import org.briarproject.android.fragment.BaseEventFragment; -import org.briarproject.android.invitation.AddContactActivity; +import org.briarproject.android.keyagreement.KeyAgreementActivity; import org.briarproject.android.util.BriarRecyclerView; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; @@ -107,7 +107,7 @@ public class ContactListFragment extends BaseEventFragment { @Override public void onClick(View v) { startActivity(new Intent(getContext(), - AddContactActivity.class)); + KeyAgreementActivity.class)); } }); diff --git a/briar-android/src/org/briarproject/android/keyagreement/ChooseIdentityFragment.java b/briar-android/src/org/briarproject/android/keyagreement/ChooseIdentityFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..4615cd728625345b440d55e4e4fa8695d3f92e38 --- /dev/null +++ b/briar-android/src/org/briarproject/android/keyagreement/ChooseIdentityFragment.java @@ -0,0 +1,193 @@ +package org.briarproject.android.keyagreement; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.Spinner; + +import org.briarproject.R; +import org.briarproject.android.AndroidComponent; +import org.briarproject.android.fragment.BaseFragment; +import org.briarproject.android.identity.CreateIdentityActivity; +import org.briarproject.android.identity.LocalAuthorItem; +import org.briarproject.android.identity.LocalAuthorItemComparator; +import org.briarproject.android.identity.LocalAuthorSpinnerAdapter; +import org.briarproject.api.db.DbException; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; + +import java.util.Collection; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static android.app.Activity.RESULT_OK; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.android.identity.LocalAuthorItem.NEW; + +public class ChooseIdentityFragment extends BaseFragment + implements OnItemSelectedListener { + + interface IdentitySelectedListener { + void identitySelected(AuthorId localAuthorId); + } + + private static final Logger LOG = + Logger.getLogger(ChooseIdentityFragment.class.getName()); + + public static final String TAG = "ChooseIdentityFragment"; + + private static final int REQUEST_CREATE_IDENTITY = 1; + + private IdentitySelectedListener lsnr; + private LocalAuthorSpinnerAdapter adapter; + private Spinner spinner; + private View button; + + private AuthorId localAuthorId; + + // Fields that are accessed from background threads must be volatile + @Inject + protected volatile IdentityManager identityManager; + + public static ChooseIdentityFragment newInstance() { + Bundle args = new Bundle(); + ChooseIdentityFragment fragment = new ChooseIdentityFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + lsnr = (IdentitySelectedListener) context; + } catch (ClassCastException e) { + throw new ClassCastException( + "Using class must implement IdentitySelectedListener"); + } + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void injectActivity(AndroidComponent component) { + component.inject(this); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.invitation_bluetooth_start, container, + false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + adapter = new LocalAuthorSpinnerAdapter(getActivity(), false); + spinner = (Spinner) view.findViewById(R.id.spinner); + spinner.setAdapter(adapter); + spinner.setOnItemSelectedListener(this); + + button = view.findViewById(R.id.continueButton); + button.setEnabled(false); + button.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + lsnr.identitySelected(localAuthorId); + } + }); + + loadLocalAuthors(); + } + + private void loadLocalAuthors() { + listener.runOnDbThread(new Runnable() { + public void run() { + try { + long now = System.currentTimeMillis(); + Collection<LocalAuthor> authors = + identityManager.getLocalAuthors(); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading authors took " + duration + " ms"); + displayLocalAuthors(authors); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private void displayLocalAuthors(final Collection<LocalAuthor> authors) { + listener.runOnUiThread(new Runnable() { + @Override + public void run() { + adapter.clear(); + for (LocalAuthor a : authors) + adapter.add(new LocalAuthorItem(a)); + adapter.sort(LocalAuthorItemComparator.INSTANCE); + // If a local author has been selected, select it again + if (localAuthorId == null) return; + int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + LocalAuthorItem item = adapter.getItem(i); + if (item == NEW) continue; + if (item.getLocalAuthor().getId().equals(localAuthorId)) { + spinner.setSelection(i); + return; + } + } + } + }); + } + + private void setLocalAuthorId(AuthorId authorId) { + localAuthorId = authorId; + button.setEnabled(localAuthorId != null); + } + + public void onItemSelected(AdapterView<?> parent, View view, int position, + long id) { + LocalAuthorItem item = adapter.getItem(position); + if (item == NEW) { + setLocalAuthorId(null); + Intent i = new Intent(getActivity(), CreateIdentityActivity.class); + startActivityForResult(i, REQUEST_CREATE_IDENTITY); + } else { + setLocalAuthorId(item.getLocalAuthor().getId()); + } + } + + public void onNothingSelected(AdapterView<?> parent) { + setLocalAuthorId(null); + } + + @Override + public void onActivityResult(int request, int result, Intent data) { + if (request == REQUEST_CREATE_IDENTITY && result == RESULT_OK) { + byte[] b = data.getByteArrayExtra("briar.LOCAL_AUTHOR_ID"); + if (b == null) throw new IllegalStateException(); + setLocalAuthorId(new AuthorId(b)); + loadLocalAuthors(); + } else + super.onActivityResult(request, result, data); + } +} diff --git a/briar-android/src/org/briarproject/android/keyagreement/KeyAgreementActivity.java b/briar-android/src/org/briarproject/android/keyagreement/KeyAgreementActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..185d3c0b1efc3d4f14daca2d1b312d417a79aadb --- /dev/null +++ b/briar-android/src/org/briarproject/android/keyagreement/KeyAgreementActivity.java @@ -0,0 +1,230 @@ +package org.briarproject.android.keyagreement; + +import android.os.Bundle; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import org.briarproject.R; +import org.briarproject.android.AndroidComponent; +import org.briarproject.android.BriarFragmentActivity; +import org.briarproject.android.fragment.BaseFragment; +import org.briarproject.android.util.CustomAnimations; +import org.briarproject.api.contact.ContactExchangeListener; +import org.briarproject.api.contact.ContactExchangeTask; +import org.briarproject.api.db.DbException; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.KeyAgreementFinishedEvent; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.keyagreement.KeyAgreementResult; +import org.briarproject.api.settings.SettingsManager; + +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static android.widget.Toast.LENGTH_LONG; +import static java.util.logging.Level.WARNING; + +public class KeyAgreementActivity extends BriarFragmentActivity implements + BaseFragment.BaseFragmentListener, + ChooseIdentityFragment.IdentitySelectedListener, EventListener, + ContactExchangeListener { + + private static final Logger LOG = + Logger.getLogger(KeyAgreementActivity.class.getName()); + + private static final String LOCAL_AUTHOR_ID = "briar.LOCAL_AUTHOR_ID"; + + private static final int STEP_ID = 1; + private static final int STEP_QR = 2; + private static final int STEPS = 2; + + @Inject + protected EventBus eventBus; + @Inject + protected SettingsManager settingsManager; + + private Toolbar toolbar; + private View progressContainer; + private TextView progressTitle; + + private AuthorId localAuthorId; + + @Inject + protected volatile ContactExchangeTask contactExchangeTask; + @Inject + protected volatile IdentityManager identityManager; + + @Override + public void injectActivity(AndroidComponent component) { + component.inject(this); + } + + @SuppressWarnings("ConstantConditions") + @Override + public void onCreate(Bundle state) { + super.onCreate(state); + setContentView(R.layout.activity_with_loading); + + toolbar = (Toolbar) findViewById(R.id.toolbar); + progressContainer = findViewById(R.id.container_progress); + progressTitle = (TextView) findViewById(R.id.title_progress_bar); + + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + if (state != null) { + byte[] b = state.getByteArray(LOCAL_AUTHOR_ID); + if (b != null) + localAuthorId = new AuthorId(b); + } + + showStep(localAuthorId == null ? STEP_ID : STEP_QR); + } + + @SuppressWarnings("ConstantConditions") + private void showStep(int step) { + getSupportActionBar().setTitle( + String.format(getString(R.string.add_contact_title_step), step, + STEPS)); + switch (step) { + case STEP_QR: + startFragment(ShowQrCodeFragment.newInstance()); + break; + case STEP_ID: + default: + startFragment(ChooseIdentityFragment.newInstance()); + break; + } + } + + @Override + public void onResume() { + super.onResume(); + eventBus.addListener(this); + } + + @Override + protected void onPause() { + super.onPause(); + eventBus.removeListener(this); + } + + @Override + public void onSaveInstanceState(Bundle state) { + super.onSaveInstanceState(state); + if (localAuthorId != null) { + byte[] b = localAuthorId.getBytes(); + state.putByteArray(LOCAL_AUTHOR_ID, b); + } + } + + @Override + public void showLoadingScreen(boolean isBlocking, int stringId) { + if (isBlocking) { + CustomAnimations.animateHeight(toolbar, false, 250); + } + progressTitle.setText(stringId); + progressContainer.setVisibility(View.VISIBLE); + } + + @Override + public void hideLoadingScreen() { + CustomAnimations.animateHeight(toolbar, true, 250); + progressContainer.setVisibility(View.INVISIBLE); + } + + @Override + public void identitySelected(AuthorId localAuthorId) { + this.localAuthorId = localAuthorId; + showStep(STEP_QR); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof KeyAgreementFinishedEvent) { + KeyAgreementFinishedEvent event = (KeyAgreementFinishedEvent) e; + keyAgreementFinished(event.getResult()); + } + } + + private void keyAgreementFinished(final KeyAgreementResult result) { + runOnUiThread(new Runnable() { + @Override + public void run() { + showLoadingScreen(false, R.string.exchanging_contact_details); + startContactExchange(result); + } + }); + } + + private void startContactExchange(final KeyAgreementResult result) { + runOnDbThread(new Runnable() { + @Override + public void run() { + LocalAuthor localAuthor; + // Load the local pseudonym + try { + localAuthor = identityManager.getLocalAuthor(localAuthorId); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + contactExchangeFailed(); + return; + } + + // Exchange contact details + contactExchangeTask.startExchange(KeyAgreementActivity.this, + localAuthor, result.getMasterKey(), + result.getConnection(), result.getTransportId(), + result.wasAlice()); + } + }); + } + + @Override + public void contactExchangeSucceeded(final Author remoteAuthor) { + runOnUiThread(new Runnable() { + public void run() { + String contactName = remoteAuthor.getName(); + String format = getString(R.string.contact_added_toast); + String text = String.format(format, contactName); + Toast.makeText(KeyAgreementActivity.this, text, LENGTH_LONG) + .show(); + finish(); + } + }); + } + + @Override + public void duplicateContact(final Author remoteAuthor) { + runOnUiThread(new Runnable() { + public void run() { + String contactName = remoteAuthor.getName(); + String format = getString(R.string.contact_already_exists); + String text = String.format(format, contactName); + Toast.makeText(KeyAgreementActivity.this, text, LENGTH_LONG) + .show(); + finish(); + } + }); + } + + @Override + public void contactExchangeFailed() { + runOnUiThread(new Runnable() { + public void run() { + Toast.makeText(KeyAgreementActivity.this, + R.string.contact_exchange_failed, LENGTH_LONG).show(); + finish(); + } + }); + } +} diff --git a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..df18a12fce80d8cf024b0599269ed2e6f4470460 --- /dev/null +++ b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java @@ -0,0 +1,371 @@ +package org.briarproject.android.keyagreement; + +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.Camera; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.util.Base64; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AlphaAnimation; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.zxing.Result; + +import org.briarproject.R; +import org.briarproject.android.AndroidComponent; +import org.briarproject.android.fragment.BaseEventFragment; +import org.briarproject.android.util.AndroidUtils; +import org.briarproject.android.util.CameraView; +import org.briarproject.android.util.QrCodeDecoder; +import org.briarproject.android.util.QrCodeUtils; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.KeyAgreementAbortedEvent; +import org.briarproject.api.event.KeyAgreementFailedEvent; +import org.briarproject.api.event.KeyAgreementListeningEvent; +import org.briarproject.api.event.KeyAgreementStartedEvent; +import org.briarproject.api.event.KeyAgreementWaitingEvent; +import org.briarproject.api.keyagreement.KeyAgreementTask; +import org.briarproject.api.keyagreement.KeyAgreementTaskFactory; +import org.briarproject.api.keyagreement.Payload; +import org.briarproject.api.keyagreement.PayloadEncoder; +import org.briarproject.api.keyagreement.PayloadParser; + +import java.io.IOException; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED; +import static android.bluetooth.BluetoothAdapter.EXTRA_STATE; +import static android.bluetooth.BluetoothAdapter.STATE_ON; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; +import static android.widget.LinearLayout.HORIZONTAL; +import static android.widget.LinearLayout.VERTICAL; +import static android.widget.Toast.LENGTH_LONG; +import static java.util.logging.Level.WARNING; + +@SuppressWarnings("deprecation") +public class ShowQrCodeFragment extends BaseEventFragment + implements QrCodeDecoder.ResultCallback { + + private static final Logger LOG = + Logger.getLogger(ShowQrCodeFragment.class.getName()); + + public static final String TAG = "ShowQrCodeFragment"; + + @Inject + protected KeyAgreementTaskFactory keyAgreementTaskFactory; + @Inject + protected PayloadEncoder payloadEncoder; + @Inject + protected PayloadParser payloadParser; + + private LinearLayout qrLayout; + private CameraView cameraView; + private TextView status; + private ImageView qrCode; + + private BluetoothStateReceiver receiver; + private QrCodeDecoder decoder; + private boolean gotRemotePayload; + + private volatile KeyAgreementTask task; + private volatile BluetoothAdapter adapter; + private volatile boolean waitingForBluetooth; + + public static ShowQrCodeFragment newInstance() { + Bundle args = new Bundle(); + ShowQrCodeFragment fragment = new ShowQrCodeFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void injectActivity(AndroidComponent component) { + component.inject(this); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_keyagreement_qr, container, + false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + qrLayout = (LinearLayout) view.findViewById(R.id.qr_layout); + cameraView = (CameraView) view.findViewById(R.id.camera_view); + status = (TextView) view.findViewById(R.id.connect_status); + qrCode = (ImageView) view.findViewById(R.id.qr_code); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR); + + decoder = new QrCodeDecoder(this); + + Display display = getActivity().getWindowManager().getDefaultDisplay(); + boolean portrait = display.getWidth() < display.getHeight(); + qrLayout.setOrientation(portrait ? VERTICAL : HORIZONTAL); + + adapter = BluetoothAdapter.getDefaultAdapter(); + } + + @Override + public void onStart() { + super.onStart(); + + // Listen for changes to the Bluetooth state + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_STATE_CHANGED); + receiver = new BluetoothStateReceiver(); + getActivity().registerReceiver(receiver, filter); + + // Enable BT adapter if it is not already on. + if (adapter != null && !adapter.isEnabled()) { + waitingForBluetooth = true; + AndroidUtils.enableBluetooth(adapter, true); + } else + startListening(); + } + + @Override + public void onResume() { + super.onResume(); + if (!gotRemotePayload) openCamera(); + } + + @Override + public void onPause() { + super.onPause(); + if (!gotRemotePayload) releaseCamera(); + } + + @Override + public void onStop() { + super.onStop(); + stopListening(); + if (receiver != null) getActivity().unregisterReceiver(receiver); + } + + private void startListening() { + task = keyAgreementTaskFactory.getTask(); + gotRemotePayload = false; + new Thread(new Runnable() { + @Override + public void run() { + task.listen(); + } + }).start(); + } + + private void stopListening() { + new Thread(new Runnable() { + @Override + public void run() { + task.stopListening(); + } + }).start(); + } + + private void openCamera() { + AsyncTask<Void, Void, Camera> openTask = + new AsyncTask<Void, Void, Camera>() { + @Override + protected Camera doInBackground(Void... unused) { + LOG.info("Opening camera"); + try { + return Camera.open(); + } catch (RuntimeException e) { + LOG.log(WARNING, + "Error opening camera, trying again", e); + try { + Thread.sleep(1000); + } catch (InterruptedException e2) { + LOG.info("Interrupted before second attempt"); + return null; + } + try { + return Camera.open(); + } catch (RuntimeException e2) { + LOG.log(WARNING, "Error opening camera", e2); + return null; + } + } + } + + @Override + protected void onPostExecute(Camera camera) { + if (camera == null) { + // TODO better solution? + getActivity().finish(); + } else { + cameraView.start(camera, decoder, 0); + } + } + }; + openTask.execute(); + } + + private void releaseCamera() { + LOG.info("Releasing camera"); + try { + cameraView.stop(); + } catch (RuntimeException e) { + LOG.log(WARNING, "Error releasing camera", e); + // TODO better solution + getActivity().finish(); + } + } + + private void reset() { + cameraView.setVisibility(View.VISIBLE); + startListening(); + openCamera(); + } + + private void qrCodeScanned(String content) { + try { + // TODO use Base32 + Payload remotePayload = payloadParser.parse( + Base64.decode(content, 0)); + cameraView.setVisibility(View.GONE); + status.setText(R.string.connecting_to_device); + task.connectAndRunProtocol(remotePayload); + } catch (IOException e) { + // TODO show failure + Toast.makeText(getActivity(), R.string.qr_code_invalid, + LENGTH_LONG).show(); + } + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof KeyAgreementListeningEvent) { + KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e; + setQrCode(event.getLocalPayload()); + } else if (e instanceof KeyAgreementFailedEvent) { + keyAgreementFailed(); + } else if (e instanceof KeyAgreementWaitingEvent) { + keyAgreementWaiting(); + } else if (e instanceof KeyAgreementStartedEvent) { + keyAgreementStarted(); + } else if (e instanceof KeyAgreementAbortedEvent) { + KeyAgreementAbortedEvent event = (KeyAgreementAbortedEvent) e; + keyAgreementAborted(event.didRemoteAbort()); + } + } + + private void setQrCode(final Payload localPayload) { + listener.runOnUiThread(new Runnable() { + @Override + public void run() { + // TODO use Base32 + String input = Base64.encodeToString( + payloadEncoder.encode(localPayload), 0); + qrCode.setImageBitmap( + QrCodeUtils.createQrCode(getActivity(), input)); + // Simple fade-in animation + AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f); + anim.setDuration(200); + qrCode.startAnimation(anim); + } + }); + } + + private void keyAgreementFailed() { + listener.runOnUiThread(new Runnable() { + @Override + public void run() { + reset(); + // TODO show failure somewhere persistent? + Toast.makeText(getActivity(), R.string.connection_failed, + LENGTH_LONG).show(); + } + }); + } + + private void keyAgreementWaiting() { + listener.runOnUiThread(new Runnable() { + @Override + public void run() { + status.setText(R.string.waiting_for_contact); + } + }); + } + + private void keyAgreementStarted() { + listener.runOnUiThread(new Runnable() { + @Override + public void run() { + listener.showLoadingScreen(false, + R.string.authenticating_with_device); + } + }); + } + + private void keyAgreementAborted(final boolean remoteAborted) { + listener.runOnUiThread(new Runnable() { + @Override + public void run() { + reset(); + listener.hideLoadingScreen(); + // TODO show abort somewhere persistent? + Toast.makeText(getActivity(), + remoteAborted ? R.string.connection_aborted_remote : + R.string.connection_aborted_local, LENGTH_LONG) + .show(); + } + }); + } + + @Override + public void handleResult(final Result result) { + listener.runOnUiThread(new Runnable() { + public void run() { + LOG.info("Got result from decoder"); + if (!gotRemotePayload) { + gotRemotePayload = true; + releaseCamera(); + qrCodeScanned(result.getText()); + } + } + }); + } + + private class BluetoothStateReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context ctx, Intent intent) { + int state = intent.getIntExtra(EXTRA_STATE, 0); + if (state == STATE_ON && waitingForBluetooth) { + LOG.info("Bluetooth enabled"); + waitingForBluetooth = false; + startListening(); + } + } + } +} diff --git a/briar-android/src/org/briarproject/android/util/CameraView.java b/briar-android/src/org/briarproject/android/util/CameraView.java new file mode 100644 index 0000000000000000000000000000000000000000..623650e6625fd13ea8e0c0a520ebb3ec0313948e --- /dev/null +++ b/briar-android/src/org/briarproject/android/util/CameraView.java @@ -0,0 +1,268 @@ +package org.briarproject.android.util; + +import android.content.Context; +import android.hardware.Camera; +import android.hardware.Camera.AutoFocusCallback; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.os.Build; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT; +import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO; +import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE; +import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO; +import static android.hardware.Camera.Parameters.FOCUS_MODE_EDOF; +import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED; +import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO; +import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE; +import static android.view.SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; + +@SuppressWarnings("deprecation") +public class CameraView extends SurfaceView implements SurfaceHolder.Callback, + AutoFocusCallback { + + private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds + private static final Logger LOG = + Logger.getLogger(CameraView.class.getName()); + + private Camera camera = null; + private PreviewConsumer previewConsumer = null; + private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0; + private boolean autoFocus = false, surfaceExists = false; + + public CameraView(Context context) { + super(context); + initialize(); + } + + public CameraView(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + public CameraView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(); + } + + private void initialize() { + setKeepScreenOn(true); + SurfaceHolder holder = getHolder(); + if (Build.VERSION.SDK_INT < 11) + holder.setType(SURFACE_TYPE_PUSH_BUFFERS); + holder.addCallback(this); + } + + public void start(Camera camera, PreviewConsumer previewConsumer, + int rotationDegrees) { + this.camera = camera; + this.previewConsumer = previewConsumer; + setDisplayOrientation(rotationDegrees); + Parameters params = camera.getParameters(); + setFocusMode(params); + setPreviewSize(params); + applyParameters(params); + if (surfaceExists) startPreview(getHolder()); + } + + public void stop() { + stopPreview(); + try { + camera.release(); + } catch (RuntimeException e) { + LOG.log(WARNING, "Error releasing camera", e); + } + camera = null; + } + + private void startPreview(SurfaceHolder holder) { + try { + camera.setPreviewDisplay(holder); + camera.startPreview(); + if (autoFocus) camera.autoFocus(this); + previewConsumer.start(camera); + } catch (IOException e) { + LOG.log(WARNING, "Error starting camera preview", e); + } catch (RuntimeException e) { + LOG.log(WARNING, "Error starting camera preview", e); + } + } + + private void stopPreview() { + try { + previewConsumer.stop(); + if (autoFocus) camera.cancelAutoFocus(); + camera.stopPreview(); + } catch (RuntimeException e) { + LOG.log(WARNING, "Error stopping camera preview", e); + } + } + + private void setDisplayOrientation(int rotationDegrees) { + int orientation; + CameraInfo info = new CameraInfo(); + Camera.getCameraInfo(0, info); + if (info.facing == CAMERA_FACING_FRONT) { + orientation = (info.orientation + rotationDegrees) % 360; + orientation = (360 - orientation) % 360; + } else { + orientation = (info.orientation - rotationDegrees + 360) % 360; + } + if(LOG.isLoggable(INFO)) + LOG.info("Display orientation " + orientation + " degrees"); + try { + camera.setDisplayOrientation(orientation); + } catch (RuntimeException e) { + LOG.log(WARNING, "Error setting display orientation", e); + } + displayOrientation = orientation; + } + + private void setFocusMode(Parameters params) { + if (Build.VERSION.SDK_INT >= 15 && + params.isVideoStabilizationSupported()) { + LOG.info("Enabling video stabilisation"); + params.setVideoStabilization(true); + } + // This returns null on the HTC Wildfire S + List<String> sceneModes = params.getSupportedSceneModes(); + if (sceneModes == null) sceneModes = Collections.emptyList(); + List<String> focusModes = params.getSupportedFocusModes(); + if (LOG.isLoggable(INFO)) { + LOG.info("Scene modes: " + sceneModes); + LOG.info("Focus modes: " + focusModes); + } + if (sceneModes.contains(SCENE_MODE_BARCODE)) { + LOG.info("Setting scene mode to barcode"); + params.setSceneMode(SCENE_MODE_BARCODE); + } else if (Build.VERSION.SDK_INT >= 14 && + focusModes.contains(FOCUS_MODE_CONTINUOUS_PICTURE)) { + LOG.info("Setting focus mode to continuous picture"); + params.setFocusMode(FOCUS_MODE_CONTINUOUS_PICTURE); + } else if (focusModes.contains(FOCUS_MODE_CONTINUOUS_VIDEO)) { + LOG.info("Setting focus mode to continuous video"); + params.setFocusMode(FOCUS_MODE_CONTINUOUS_VIDEO); + } else if (focusModes.contains(FOCUS_MODE_EDOF)) { + LOG.info("Setting focus mode to EDOF"); + params.setFocusMode(FOCUS_MODE_EDOF); + } else if (focusModes.contains(FOCUS_MODE_MACRO)) { + LOG.info("Setting focus mode to macro"); + params.setFocusMode(FOCUS_MODE_MACRO); + autoFocus = true; + } else if (focusModes.contains(FOCUS_MODE_AUTO)) { + LOG.info("Setting focus mode to auto"); + params.setFocusMode(FOCUS_MODE_AUTO); + autoFocus = true; + } else if (focusModes.contains(FOCUS_MODE_FIXED)) { + LOG.info("Setting focus mode to fixed"); + params.setFocusMode(FOCUS_MODE_FIXED); + } else { + LOG.info("No suitable focus mode"); + } + params.setZoom(0); + } + + private void setPreviewSize(Parameters params) { + if (surfaceWidth == 0 || surfaceHeight == 0) return; + float idealRatio = (float) surfaceWidth / surfaceHeight; + DisplayMetrics screen = getContext().getResources().getDisplayMetrics(); + int screenMax = Math.max(screen.widthPixels, screen.heightPixels); + boolean rotatePreview = displayOrientation % 180 == 90; + List<Size> sizes = params.getSupportedPreviewSizes(); + Size bestSize = null; + float bestScore = 0; + for (Size size : sizes) { + int width = rotatePreview ? size.height : size.width; + int height = rotatePreview ? size.width : size.height; + float ratio = (float) width / height; + float stretch = Math.max(ratio / idealRatio, idealRatio / ratio); + int pixels = width * height; + float score = width * height / stretch; + if (LOG.isLoggable(INFO)) { + LOG.info("Size " + size.width + "x" + size.height + + ", stretch " + stretch + ", pixels " + pixels + + ", score " + score); + } + // Large preview sizes can crash older devices + int maxDimension = Math.max(width, height); + if (Build.VERSION.SDK_INT < 14 && maxDimension > screenMax) { + LOG.info("Too large for screen"); + continue; + } + if (bestSize == null || score > bestScore) { + bestSize = size; + bestScore = score; + } + } + if (bestSize != null) { + if(LOG.isLoggable(INFO)) + LOG.info("Best size " + bestSize.width + "x" + bestSize.height); + params.setPreviewSize(bestSize.width, bestSize.height); + } + } + + private void applyParameters(Parameters params) { + try { + camera.setParameters(params); + } catch (RuntimeException e) { + LOG.log(WARNING, "Error setting camera parameters", e); + } + } + + public void surfaceCreated(SurfaceHolder holder) { + LOG.info("Surface created"); + surfaceExists = true; + if (camera != null) startPreview(holder); + } + + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + if (LOG.isLoggable(INFO)) LOG.info("Surface changed: " + w + "x" + h); + surfaceWidth = w; + surfaceHeight = h; + if (camera == null) return; // We are stopped + stopPreview(); + try { + Parameters params = camera.getParameters(); + setPreviewSize(params); + applyParameters(params); + } catch (RuntimeException e) { + LOG.log(WARNING, "Error getting camera parameters", e); + } + startPreview(holder); + } + + public void surfaceDestroyed(SurfaceHolder holder) { + LOG.info("Surface destroyed"); + surfaceExists = false; + holder.removeCallback(this); + } + + public void onAutoFocus(boolean success, final Camera camera) { + LOG.info("Auto focus succeeded: " + success); + postDelayed(new Runnable() { + public void run() { + retryAutoFocus(); + } + }, AUTO_FOCUS_RETRY_DELAY); + } + + private void retryAutoFocus() { + try { + if (camera != null) camera.autoFocus(this); + } catch (RuntimeException e) { + LOG.log(WARNING, "Error retrying auto focus", e); + } + } +} \ No newline at end of file diff --git a/briar-android/src/org/briarproject/android/util/PreviewConsumer.java b/briar-android/src/org/briarproject/android/util/PreviewConsumer.java new file mode 100644 index 0000000000000000000000000000000000000000..e2fb6eed9556dc44bcb79246f9aaf6179ac112f9 --- /dev/null +++ b/briar-android/src/org/briarproject/android/util/PreviewConsumer.java @@ -0,0 +1,11 @@ +package org.briarproject.android.util; + +import android.hardware.Camera; + +@SuppressWarnings("deprecation") +public interface PreviewConsumer { + + void start(Camera camera); + + void stop(); +} diff --git a/briar-android/src/org/briarproject/android/util/QrCodeDecoder.java b/briar-android/src/org/briarproject/android/util/QrCodeDecoder.java new file mode 100644 index 0000000000000000000000000000000000000000..5fdc54c39478a45b8c56ccd68aa8296406092252 --- /dev/null +++ b/briar-android/src/org/briarproject/android/util/QrCodeDecoder.java @@ -0,0 +1,100 @@ +package org.briarproject.android.util; + +import android.hardware.Camera; +import android.hardware.Camera.PreviewCallback; +import android.hardware.Camera.Size; +import android.os.AsyncTask; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.LuminanceSource; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.Reader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.qrcode.QRCodeReader; + +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; + +@SuppressWarnings("deprecation") +public class QrCodeDecoder implements PreviewConsumer, PreviewCallback { + + private static final Logger LOG = + Logger.getLogger(QrCodeDecoder.class.getName()); + + private final Reader reader = new QRCodeReader(); + private final ResultCallback callback; + + private boolean stopped = false; + + public QrCodeDecoder(ResultCallback callback) { + this.callback = callback; + } + + public void start(Camera camera) { + stopped = false; + askForPreviewFrame(camera); + } + + public void stop() { + stopped = true; + } + + private void askForPreviewFrame(Camera camera) { + if (!stopped) camera.setOneShotPreviewCallback(this); + } + + public void onPreviewFrame(byte[] data, Camera camera) { + if (!stopped) { + Size size = camera.getParameters().getPreviewSize(); + new DecoderTask(camera, data, size.width, size.height).execute(); + } + } + + private class DecoderTask extends AsyncTask<Void, Void, Void> { + + final Camera camera; + final byte[] data; + final int width, height; + + DecoderTask(Camera camera, byte[] data, int width, int height) { + this.camera = camera; + this.data = data; + this.width = width; + this.height = height; + } + + @Override + protected Void doInBackground(Void... params) { + long now = System.currentTimeMillis(); + LuminanceSource src = new PlanarYUVLuminanceSource(data, width, + height, 0, 0, width, height, false); + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src)); + Result result = null; + try { + result = reader.decode(bitmap); + } catch (ReaderException e) { + return null; // No barcode found + } finally { + reader.reset(); + } + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Decoding barcode took " + duration + " ms"); + callback.handleResult(result); + return null; + } + + @Override + protected void onPostExecute(Void result) { + askForPreviewFrame(camera); + } + } + + public interface ResultCallback { + + void handleResult(Result result); + } +} diff --git a/briar-android/src/org/briarproject/android/util/QrCodeUtils.java b/briar-android/src/org/briarproject/android/util/QrCodeUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..ef8ff6ec6714e4c6a8f444d6c26a011053a9f21d --- /dev/null +++ b/briar-android/src/org/briarproject/android/util/QrCodeUtils.java @@ -0,0 +1,51 @@ +package org.briarproject.android.util; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.util.DisplayMetrics; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; + +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; + +public class QrCodeUtils { + + private static final Logger LOG = + Logger.getLogger(QrCodeUtils.class.getName()); + + public static Bitmap createQrCode(Activity activity, String input) { + // Get narrowest screen dimension + DisplayMetrics dm = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(dm); + int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels); + try { + // Generate QR code + final BitMatrix encoded = new QRCodeWriter().encode( + input, BarcodeFormat.QR_CODE, smallestDimen, smallestDimen); + // Convert QR code to Bitmap + int width = encoded.getWidth(); + int height = encoded.getHeight(); + int[] pixels = new int[width * height]; + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + pixels[y * width + x] = + encoded.get(x, y) ? Color.BLACK : Color.WHITE; + } + } + Bitmap qr = Bitmap.createBitmap(width, height, + Bitmap.Config.ARGB_8888); + qr.setPixels(pixels, 0, width, 0, 0, width, height); + return qr; + } catch (WriterException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + return null; + } + } +} diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java index 2bc7dbe702d2ddbeba33e540373dd5345a46d398..40a8a041063d78cdd1403c99e99a1603b8dea026 100644 --- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java +++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java @@ -14,6 +14,9 @@ import org.briarproject.api.TransportId; import org.briarproject.android.api.AndroidExecutor; import org.briarproject.api.contact.ContactId; import org.briarproject.api.crypto.PseudoRandom; +import org.briarproject.api.keyagreement.KeyAgreementConnection; +import org.briarproject.api.keyagreement.KeyAgreementListener; +import org.briarproject.api.keyagreement.TransportDescriptor; import org.briarproject.api.plugins.Backoff; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; @@ -67,6 +70,9 @@ class DroidtoothPlugin implements DuplexPlugin { private static final String DISCOVERY_FINISHED = "android.bluetooth.adapter.action.DISCOVERY_FINISHED"; + private static final String PROP_ADDRESS = "address"; + private static final String PROP_UUID = "uuid"; + private final Executor ioExecutor; private final AndroidExecutor androidExecutor; private final Context appContext; @@ -161,7 +167,7 @@ class DroidtoothPlugin implements DuplexPlugin { if (!StringUtils.isNullOrEmpty(address)) { // Advertise the Bluetooth address to contacts TransportProperties p = new TransportProperties(); - p.put("address", address); + p.put(PROP_ADDRESS, address); callback.mergeLocalProperties(p); } // Bind a server socket to accept connections from contacts @@ -187,13 +193,13 @@ class DroidtoothPlugin implements DuplexPlugin { } private UUID getUuid() { - String uuid = callback.getLocalProperties().get("uuid"); + String uuid = callback.getLocalProperties().get(PROP_UUID); if (uuid == null) { byte[] random = new byte[UUID_BYTES]; secureRandom.nextBytes(random); uuid = UUID.nameUUIDFromBytes(random).toString(); TransportProperties p = new TransportProperties(); - p.put("uuid", uuid); + p.put(PROP_UUID, uuid); callback.mergeLocalProperties(p); } return UUID.fromString(uuid); @@ -264,9 +270,9 @@ class DroidtoothPlugin implements DuplexPlugin { for (Entry<ContactId, TransportProperties> e : remote.entrySet()) { final ContactId c = e.getKey(); if (connected.contains(c)) continue; - final String address = e.getValue().get("address"); + final String address = e.getValue().get(PROP_ADDRESS); if (StringUtils.isNullOrEmpty(address)) continue; - final String uuid = e.getValue().get("uuid"); + final String uuid = e.getValue().get(PROP_UUID); if (StringUtils.isNullOrEmpty(uuid)) continue; ioExecutor.execute(new Runnable() { public void run() { @@ -325,9 +331,9 @@ class DroidtoothPlugin implements DuplexPlugin { if (!isRunning()) return null; TransportProperties p = callback.getRemoteProperties().get(c); if (p == null) return null; - String address = p.get("address"); + String address = p.get(PROP_ADDRESS); if (StringUtils.isNullOrEmpty(address)) return null; - String uuid = p.get("uuid"); + String uuid = p.get(PROP_UUID); if (StringUtils.isNullOrEmpty(uuid)) return null; BluetoothSocket s = connect(address, uuid); if (s == null) return null; @@ -417,6 +423,48 @@ class DroidtoothPlugin implements DuplexPlugin { }); } + public boolean supportsKeyAgreement() { + return true; + } + + public KeyAgreementListener createKeyAgreementListener( + byte[] localCommitment) { + // No truncation necessary because COMMIT_LENGTH = 16 + UUID uuid = UUID.nameUUIDFromBytes(localCommitment); + if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid); + // Bind a server socket for receiving invitation connections + BluetoothServerSocket ss; + try { + ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } + TransportProperties p = new TransportProperties(); + String address = AndroidUtils.getBluetoothAddress(appContext, adapter); + if (!StringUtils.isNullOrEmpty(address)) + p.put(PROP_ADDRESS, address); + TransportDescriptor d = new TransportDescriptor(ID, p); + return new BluetoothKeyAgreementListener(d, ss); + } + + public DuplexTransportConnection createKeyAgreementConnection( + byte[] remoteCommitment, TransportDescriptor d, long timeout) { + if (!isRunning()) return null; + if (!ID.equals(d.getIdentifier())) return null; + TransportProperties p = d.getProperties(); + if (p == null) return null; + String address = p.get(PROP_ADDRESS); + if (StringUtils.isNullOrEmpty(address)) return null; + // No truncation necessary because COMMIT_LENGTH = 16 + UUID uuid = UUID.nameUUIDFromBytes(remoteCommitment); + if (LOG.isLoggable(INFO)) + LOG.info("Connecting to key agreement UUID " + uuid); + BluetoothSocket s = connect(address, uuid.toString()); + if (s == null) return null; + return new DroidtoothTransportConnection(this, s); + } + private class BluetoothStateReceiver extends BroadcastReceiver { @Override @@ -545,4 +593,39 @@ class DroidtoothPlugin implements DuplexPlugin { return s; } } + + private class BluetoothKeyAgreementListener extends KeyAgreementListener { + + private final BluetoothServerSocket ss; + + public BluetoothKeyAgreementListener(TransportDescriptor descriptor, + BluetoothServerSocket ss) { + super(descriptor); + this.ss = ss; + } + + @Override + public Callable<KeyAgreementConnection> listen() { + return new Callable<KeyAgreementConnection>() { + @Override + public KeyAgreementConnection call() throws IOException { + BluetoothSocket s = ss.accept(); + if (LOG.isLoggable(INFO)) + LOG.info(ID.getString() + ": Incoming connection"); + return new KeyAgreementConnection( + new DroidtoothTransportConnection( + DroidtoothPlugin.this, s), ID); + } + }; + } + + @Override + public void close() { + try { + ss.close(); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + } } diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java index e5f7180c0e9f91ff459fb941bbc0c2d877bc461b..d3dbff7932243505f43d9426debf3bcec0f597b4 100644 --- a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java +++ b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java @@ -19,6 +19,8 @@ import org.briarproject.api.crypto.PseudoRandom; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventListener; import org.briarproject.api.event.SettingsUpdatedEvent; +import org.briarproject.api.keyagreement.KeyAgreementListener; +import org.briarproject.api.keyagreement.TransportDescriptor; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexTransportConnection; @@ -570,6 +572,20 @@ class TorPlugin implements DuplexPlugin, EventHandler, throw new UnsupportedOperationException(); } + public boolean supportsKeyAgreement() { + return false; + } + + public KeyAgreementListener createKeyAgreementListener( + byte[] commitment) { + throw new UnsupportedOperationException(); + } + + public DuplexTransportConnection createKeyAgreementConnection( + byte[] commitment, TransportDescriptor d, long timeout) { + throw new UnsupportedOperationException(); + } + public void circuitStatus(String status, String id, String path) { if (status.equals("BUILT") && !circuitBuilt.getAndSet(true)) { LOG.info("First circuit built"); diff --git a/briar-api/src/org/briarproject/api/contact/ContactExchangeListener.java b/briar-api/src/org/briarproject/api/contact/ContactExchangeListener.java new file mode 100644 index 0000000000000000000000000000000000000000..50bf543c71c26607f6c041cbff59c23df152e85b --- /dev/null +++ b/briar-api/src/org/briarproject/api/contact/ContactExchangeListener.java @@ -0,0 +1,14 @@ +package org.briarproject.api.contact; + +import org.briarproject.api.identity.Author; + +public interface ContactExchangeListener { + + void contactExchangeSucceeded(Author remoteAuthor); + + /** The exchange failed because the contact already exists. */ + void duplicateContact(Author remoteAuthor); + + /** A general failure. */ + void contactExchangeFailed(); +} diff --git a/briar-api/src/org/briarproject/api/contact/ContactExchangeTask.java b/briar-api/src/org/briarproject/api/contact/ContactExchangeTask.java new file mode 100644 index 0000000000000000000000000000000000000000..0d85850f6fa8a67b9401396854652ce3bec47c02 --- /dev/null +++ b/briar-api/src/org/briarproject/api/contact/ContactExchangeTask.java @@ -0,0 +1,20 @@ +package org.briarproject.api.contact; + +import org.briarproject.api.TransportId; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.plugins.duplex.DuplexTransportConnection; + +/** + * A task for conducting a contact information exchange with a remote peer. + */ +public interface ContactExchangeTask { + + /** + * Exchange contact information with a remote peer. + */ + void startExchange(ContactExchangeListener listener, + LocalAuthor localAuthor, SecretKey masterSecret, + DuplexTransportConnection conn, TransportId transportId, + boolean alice); +} diff --git a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java index 0ba779202b8d6eea5975a958a28cb22bbb42d215..11631f22fda6db0351682106a9f0828d2c02e20f 100644 --- a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java +++ b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java @@ -36,18 +36,17 @@ public interface CryptoComponent { int deriveBTConfirmationCode(SecretKey master, boolean alice); /** - * Derives a header key for an invitation stream from the given master - * secret. + * Derives a stream header key from the given master secret. * @param alice whether the key is for use by Alice or Bob. */ - SecretKey deriveBTInvitationKey(SecretKey master, boolean alice); + SecretKey deriveHeaderKey(SecretKey master, boolean alice); /** * Derives a nonce from the given master secret for one of the parties to * sign. * @param alice whether the nonce is for use by Alice or Bob. */ - byte[] deriveBTSignatureNonce(SecretKey master, boolean alice); + byte[] deriveSignatureNonce(SecretKey master, boolean alice); /** * Derives a commitment to the provided public key. @@ -107,7 +106,7 @@ public interface CryptoComponent { * Derives a master secret from two public keys and one of the corresponding * private keys. * <p/> - * Part of BQP. This is a helper method that calls + * This is a helper method that calls * deriveMasterSecret(deriveSharedSecret(theirPublicKey, ourKeyPair, alice)) * * @param theirPublicKey the ephemeral public key of the remote party diff --git a/briar-api/src/org/briarproject/api/event/KeyAgreementAbortedEvent.java b/briar-api/src/org/briarproject/api/event/KeyAgreementAbortedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..5e217f7f4d2752f09ae68d7e0ee9b9cbc68f9d46 --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/KeyAgreementAbortedEvent.java @@ -0,0 +1,15 @@ +package org.briarproject.api.event; + +/** An event that is broadcast when a BQP protocol aborts. */ +public class KeyAgreementAbortedEvent extends Event { + + private final boolean remoteAborted; + + public KeyAgreementAbortedEvent(boolean remoteAborted) { + this.remoteAborted = remoteAborted; + } + + public boolean didRemoteAbort() { + return remoteAborted; + } +} diff --git a/briar-api/src/org/briarproject/api/event/KeyAgreementFailedEvent.java b/briar-api/src/org/briarproject/api/event/KeyAgreementFailedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..97313a276b0bf19f582e4177b3680753ac09568f --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/KeyAgreementFailedEvent.java @@ -0,0 +1,6 @@ +package org.briarproject.api.event; + +/** An event that is broadcast when a BQP connection cannot be created. */ +public class KeyAgreementFailedEvent extends Event { + +} diff --git a/briar-api/src/org/briarproject/api/event/KeyAgreementFinishedEvent.java b/briar-api/src/org/briarproject/api/event/KeyAgreementFinishedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..f2afe349ebb23c6a9c63d33d518660f7b84e036b --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/KeyAgreementFinishedEvent.java @@ -0,0 +1,17 @@ +package org.briarproject.api.event; + +import org.briarproject.api.keyagreement.KeyAgreementResult; + +/** An event that is broadcast when a BQP protocol completes. */ +public class KeyAgreementFinishedEvent extends Event { + + private final KeyAgreementResult result; + + public KeyAgreementFinishedEvent(KeyAgreementResult result) { + this.result = result; + } + + public KeyAgreementResult getResult() { + return result; + } +} diff --git a/briar-api/src/org/briarproject/api/event/KeyAgreementListeningEvent.java b/briar-api/src/org/briarproject/api/event/KeyAgreementListeningEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..df3b2cbac5fc6ced89e43a37cee90766a7a2e70b --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/KeyAgreementListeningEvent.java @@ -0,0 +1,17 @@ +package org.briarproject.api.event; + +import org.briarproject.api.keyagreement.Payload; + +/** An event that is broadcast when a BQP task is listening. */ +public class KeyAgreementListeningEvent extends Event { + + private final Payload localPayload; + + public KeyAgreementListeningEvent(Payload localPayload) { + this.localPayload = localPayload; + } + + public Payload getLocalPayload() { + return localPayload; + } +} diff --git a/briar-api/src/org/briarproject/api/event/KeyAgreementStartedEvent.java b/briar-api/src/org/briarproject/api/event/KeyAgreementStartedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..a12a9c459a38c3b1131bb605f1b9eb857a13d2f2 --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/KeyAgreementStartedEvent.java @@ -0,0 +1,6 @@ +package org.briarproject.api.event; + +/** An event that is broadcast when a BQP protocol completes. */ +public class KeyAgreementStartedEvent extends Event { + +} diff --git a/briar-api/src/org/briarproject/api/event/KeyAgreementWaitingEvent.java b/briar-api/src/org/briarproject/api/event/KeyAgreementWaitingEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..473c6538adb053d4aeb0551621ebf8a8b2c97aa2 --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/KeyAgreementWaitingEvent.java @@ -0,0 +1,9 @@ +package org.briarproject.api.event; + +/** + * An event that is broadcast when a BQP protocol is waiting on the remote + * peer to start. + */ +public class KeyAgreementWaitingEvent extends Event { + +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConnection.java b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConnection.java new file mode 100644 index 0000000000000000000000000000000000000000..7dfa3bc689d4a288ce01b90de6e10f3e3bd50e7f --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConnection.java @@ -0,0 +1,23 @@ +package org.briarproject.api.keyagreement; + +import org.briarproject.api.TransportId; +import org.briarproject.api.plugins.duplex.DuplexTransportConnection; + +public class KeyAgreementConnection { + private final DuplexTransportConnection conn; + private final TransportId id; + + public KeyAgreementConnection(DuplexTransportConnection conn, + TransportId id) { + this.conn = conn; + this.id = id; + } + + public DuplexTransportConnection getConnection() { + return conn; + } + + public TransportId getTransportId() { + return id; + } +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConstants.java b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConstants.java index 521789244d9a8e61ae5926ea24509ebde82fbf4f..f239111269124e8d938f15bbc5561a4670ed5d0a 100644 --- a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConstants.java +++ b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConstants.java @@ -3,6 +3,17 @@ package org.briarproject.api.keyagreement; public interface KeyAgreementConstants { + /** The current version of the BQP protocol. */ + byte PROTOCOL_VERSION = 1; + + /** The length of the record header in bytes. */ + int RECORD_HEADER_LENGTH = 4; + + /** The offset of the payload length in the record header, in bytes. */ + int RECORD_HEADER_PAYLOAD_LENGTH_OFFSET = 2; + /** The length of the BQP key commitment in bytes. */ int COMMIT_LENGTH = 16; + + long CONNECTION_TIMEOUT = 20 * 1000; // Milliseconds } diff --git a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementListener.java b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementListener.java new file mode 100644 index 0000000000000000000000000000000000000000..05163614cc13d2ab383c1041813fc48b63977ae0 --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementListener.java @@ -0,0 +1,35 @@ +package org.briarproject.api.keyagreement; + +import java.util.concurrent.Callable; + +/** + * An class for managing a particular key agreement listener. + */ +public abstract class KeyAgreementListener { + + private final TransportDescriptor descriptor; + + public KeyAgreementListener(TransportDescriptor descriptor) { + this.descriptor = descriptor; + } + + /** + * Returns the descriptor that a remote peer can use to connect to this + * listener. + */ + public TransportDescriptor getDescriptor() { + return descriptor; + } + + /** + * Starts listening for incoming connections, and returns a Callable that + * will return a KeyAgreementConnection when an incoming connection is + * received. + */ + public abstract Callable<KeyAgreementConnection> listen(); + + /** + * Closes the underlying server socket. + */ + public abstract void close(); +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementResult.java b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementResult.java new file mode 100644 index 0000000000000000000000000000000000000000..3f772ca28792d58571a82b5da7b6b65a1ab27624 --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementResult.java @@ -0,0 +1,38 @@ +package org.briarproject.api.keyagreement; + +import org.briarproject.api.TransportId; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.plugins.duplex.DuplexTransportConnection; + +public class KeyAgreementResult { + + private final SecretKey masterKey; + private final DuplexTransportConnection connection; + private final TransportId transportId; + private final boolean alice; + + public KeyAgreementResult(SecretKey masterKey, + DuplexTransportConnection connection, TransportId transportId, + boolean alice) { + this.masterKey = masterKey; + this.connection = connection; + this.transportId = transportId; + this.alice = alice; + } + + public SecretKey getMasterKey() { + return masterKey; + } + + public DuplexTransportConnection getConnection() { + return connection; + } + + public TransportId getTransportId() { + return transportId; + } + + public boolean wasAlice() { + return alice; + } +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementTask.java b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementTask.java new file mode 100644 index 0000000000000000000000000000000000000000..443ef42bd68d11f7bae75cf576d8e2721bc0a63f --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementTask.java @@ -0,0 +1,21 @@ +package org.briarproject.api.keyagreement; + +/** A task for conducting a key agreement with a remote peer. */ +public interface KeyAgreementTask { + + /** + * Start listening for short-range BQP connections, if we are not already. + * <p/> + * Will trigger a KeyAgreementListeningEvent containing the local Payload, + * even if we are already listening. + */ + void listen(); + + /** + * Stop listening for short-range BQP connections. + */ + void stopListening(); + + /** Asynchronously start the connection process. */ + void connectAndRunProtocol(Payload remotePayload); +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementTaskFactory.java b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementTaskFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..f823fcacdbfe44898399ad5851f62a86c542e06e --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementTaskFactory.java @@ -0,0 +1,8 @@ +package org.briarproject.api.keyagreement; + +/** Manages tasks for conducting key agreements with remote peers. */ +public interface KeyAgreementTaskFactory { + + /** Gets the current key agreement task. */ + KeyAgreementTask getTask(); +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementTaskId.java b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementTaskId.java new file mode 100644 index 0000000000000000000000000000000000000000..434f9cb71ac2ea2bd746fd6dd225d294b1f854bb --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementTaskId.java @@ -0,0 +1,18 @@ +package org.briarproject.api.keyagreement; + +import org.briarproject.api.UniqueId; + +/** + * Type-safe wrapper for a byte array that uniquely identifies a BQP task. + */ +public class KeyAgreementTaskId extends UniqueId { + + public KeyAgreementTaskId(byte[] id) { + super(id); + } + + @Override + public boolean equals(Object o) { + return o instanceof KeyAgreementTaskId && super.equals(o); + } +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/Payload.java b/briar-api/src/org/briarproject/api/keyagreement/Payload.java new file mode 100644 index 0000000000000000000000000000000000000000..0c749da53ec9e2a70c652cc3dcdf70f5a9d079a6 --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/Payload.java @@ -0,0 +1,34 @@ +package org.briarproject.api.keyagreement; + +import org.briarproject.api.Bytes; + +import java.util.List; + +/** + * A BQP payload. + */ +public class Payload implements Comparable<Payload> { + + private final Bytes commitment; + private final List<TransportDescriptor> descriptors; + + public Payload(byte[] commitment, List<TransportDescriptor> descriptors) { + this.commitment = new Bytes(commitment); + this.descriptors = descriptors; + } + + /** Returns the commitment contained in this payload. */ + public byte[] getCommitment() { + return commitment.getBytes(); + } + + /** Returns the transport descriptors contained in this payload. */ + public List<TransportDescriptor> getTransportDescriptors() { + return descriptors; + } + + @Override + public int compareTo(Payload p) { + return commitment.compareTo(p.commitment); + } +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/PayloadEncoder.java b/briar-api/src/org/briarproject/api/keyagreement/PayloadEncoder.java new file mode 100644 index 0000000000000000000000000000000000000000..31876c10271146fe99de491bd6d3f1053ad769da --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/PayloadEncoder.java @@ -0,0 +1,6 @@ +package org.briarproject.api.keyagreement; + +public interface PayloadEncoder { + + byte[] encode(Payload p); +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/PayloadParser.java b/briar-api/src/org/briarproject/api/keyagreement/PayloadParser.java new file mode 100644 index 0000000000000000000000000000000000000000..0df9c653d418d464af843f74547981bc06f7a223 --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/PayloadParser.java @@ -0,0 +1,8 @@ +package org.briarproject.api.keyagreement; + +import java.io.IOException; + +public interface PayloadParser { + + Payload parse(byte[] raw) throws IOException; +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/RecordTypes.java b/briar-api/src/org/briarproject/api/keyagreement/RecordTypes.java new file mode 100644 index 0000000000000000000000000000000000000000..ef1d2ff51feb21d04b9e995474e091aede397379 --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/RecordTypes.java @@ -0,0 +1,9 @@ +package org.briarproject.api.keyagreement; + +/** Record types for BQP. */ +public interface RecordTypes { + + byte KEY = 0; + byte CONFIRM = 1; + byte ABORT = 2; +} diff --git a/briar-api/src/org/briarproject/api/keyagreement/TransportDescriptor.java b/briar-api/src/org/briarproject/api/keyagreement/TransportDescriptor.java new file mode 100644 index 0000000000000000000000000000000000000000..cdaa5a579464e7287f06ada392cb16d9b2ffcb5d --- /dev/null +++ b/briar-api/src/org/briarproject/api/keyagreement/TransportDescriptor.java @@ -0,0 +1,28 @@ +package org.briarproject.api.keyagreement; + +import org.briarproject.api.TransportId; +import org.briarproject.api.properties.TransportProperties; + +/** + * Describes how to connect to a device over a short-range transport. + */ +public class TransportDescriptor { + + private final TransportId id; + private final TransportProperties properties; + + public TransportDescriptor(TransportId id, TransportProperties properties) { + this.id = id; + this.properties = properties; + } + + /** Returns the transport identifier. */ + public TransportId getIdentifier() { + return id; + } + + /** Returns the transport properties. */ + public TransportProperties getProperties() { + return properties; + } +} diff --git a/briar-api/src/org/briarproject/api/plugins/PluginManager.java b/briar-api/src/org/briarproject/api/plugins/PluginManager.java index 35248962d65da3a6e6d2de481a21e4c08f874f7b..838c66ff86e1ac74a41c507f9d48b6774038c247 100644 --- a/briar-api/src/org/briarproject/api/plugins/PluginManager.java +++ b/briar-api/src/org/briarproject/api/plugins/PluginManager.java @@ -19,4 +19,7 @@ public interface PluginManager { /** Returns any running duplex plugins that support invitations. */ Collection<DuplexPlugin> getInvitationPlugins(); + + /** Returns any running duplex plugins that support key agreement. */ + Collection<DuplexPlugin> getKeyAgreementPlugins(); } diff --git a/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java b/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java index 519400b5639fb2cc598dad8f88b16b7433853feb..ff7db3b51d78526d10b2a54bcc342c3a691583c2 100644 --- a/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java +++ b/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java @@ -2,6 +2,8 @@ package org.briarproject.api.plugins.duplex; import org.briarproject.api.contact.ContactId; import org.briarproject.api.crypto.PseudoRandom; +import org.briarproject.api.keyagreement.KeyAgreementListener; +import org.briarproject.api.keyagreement.TransportDescriptor; import org.briarproject.api.plugins.Plugin; /** An interface for transport plugins that support duplex communication. */ @@ -24,4 +26,19 @@ public interface DuplexPlugin extends Plugin { */ DuplexTransportConnection createInvitationConnection(PseudoRandom r, long timeout, boolean alice); + + /** Returns true if the plugin supports short-range key agreement. */ + boolean supportsKeyAgreement(); + + /** + * Returns a listener that can be used to perform key agreement. + */ + KeyAgreementListener createKeyAgreementListener(byte[] localCommitment); + + /** + * Attempts to connect to the remote peer specified in the given descriptor. + * Returns null if no connection can be established within the given time. + */ + DuplexTransportConnection createKeyAgreementConnection( + byte[] remoteCommitment, TransportDescriptor d, long timeout); } diff --git a/briar-core/build.gradle b/briar-core/build.gradle index 7fe4201a76c6f6e93d5f4aa4857015fcdf92e0cf..94452e1e08860a5dfc8fd53f3bc6b2e4515e7402 100644 --- a/briar-core/build.gradle +++ b/briar-core/build.gradle @@ -13,12 +13,14 @@ dependencies { compile fileTree(dir: 'libs', include: '*.jar') compile "com.madgag.spongycastle:core:1.54.0.0" compile "com.h2database:h2:1.4.190" + compile "com.google.zxing:core:3.2.1" } dependencyVerification { verify = [ 'com.madgag.spongycastle:core:1e7fa4b19ccccd1011364ab838d0b4702470c178bbbdd94c5c90b2d4d749ea1e', 'com.h2database:h2:23ba495a07bbbb3bd6c3084d10a96dad7a23741b8b6d64b213459a784195a98c', + 'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259', ] } diff --git a/briar-core/src/org/briarproject/CoreModule.java b/briar-core/src/org/briarproject/CoreModule.java index 11d6c87b0ac55d85e9a16c3e2de12a872c8b41d4..6303f210b3129e5a28e7121509aa423101c5c03c 100644 --- a/briar-core/src/org/briarproject/CoreModule.java +++ b/briar-core/src/org/briarproject/CoreModule.java @@ -9,6 +9,7 @@ import org.briarproject.event.EventModule; import org.briarproject.forum.ForumModule; import org.briarproject.identity.IdentityModule; import org.briarproject.invitation.InvitationModule; +import org.briarproject.keyagreement.KeyAgreementModule; import org.briarproject.lifecycle.LifecycleModule; import org.briarproject.messaging.MessagingModule; import org.briarproject.plugins.PluginsModule; @@ -23,7 +24,8 @@ import dagger.Module; @Module(includes = {DatabaseModule.class, CryptoModule.class, LifecycleModule.class, ReliabilityModule.class, - MessagingModule.class, InvitationModule.class, ForumModule.class, + MessagingModule.class, InvitationModule.class, KeyAgreementModule.class, + ForumModule.class, IdentityModule.class, EventModule.class, DataModule.class, ContactModule.class, PropertiesModule.class, TransportModule.class, SyncModule.class, SettingsModule.class, ClientsModule.class, diff --git a/briar-core/src/org/briarproject/contact/ContactExchangeTaskImpl.java b/briar-core/src/org/briarproject/contact/ContactExchangeTaskImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..943d02de98598359dedbf1767411a6334b8ed504 --- /dev/null +++ b/briar-core/src/org/briarproject/contact/ContactExchangeTaskImpl.java @@ -0,0 +1,248 @@ +package org.briarproject.contact; + +import org.briarproject.api.FormatException; +import org.briarproject.api.TransportId; +import org.briarproject.api.contact.ContactExchangeListener; +import org.briarproject.api.contact.ContactExchangeTask; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyParser; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.crypto.Signature; +import org.briarproject.api.data.BdfReader; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.BdfWriter; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.db.ContactExistsException; +import org.briarproject.api.db.DbException; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.plugins.ConnectionManager; +import org.briarproject.api.plugins.duplex.DuplexTransportConnection; +import org.briarproject.api.system.Clock; +import org.briarproject.api.transport.StreamReaderFactory; +import org.briarproject.api.transport.StreamWriterFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; + +public class ContactExchangeTaskImpl extends Thread + implements ContactExchangeTask { + + private static final Logger LOG = + Logger.getLogger(ContactExchangeTaskImpl.class.getName()); + + private final AuthorFactory authorFactory; + private final BdfReaderFactory bdfReaderFactory; + private final BdfWriterFactory bdfWriterFactory; + private final Clock clock; + private final ConnectionManager connectionManager; + private final ContactManager contactManager; + private final CryptoComponent crypto; + private final StreamReaderFactory streamReaderFactory; + private final StreamWriterFactory streamWriterFactory; + + private ContactExchangeListener listener; + private LocalAuthor localAuthor; + private DuplexTransportConnection conn; + private TransportId transportId; + private SecretKey masterSecret; + private boolean alice; + + public ContactExchangeTaskImpl(AuthorFactory authorFactory, + BdfReaderFactory bdfReaderFactory, + BdfWriterFactory bdfWriterFactory, Clock clock, + ConnectionManager connectionManager, ContactManager contactManager, + CryptoComponent crypto, StreamReaderFactory streamReaderFactory, + StreamWriterFactory streamWriterFactory) { + this.authorFactory = authorFactory; + this.bdfReaderFactory = bdfReaderFactory; + this.bdfWriterFactory = bdfWriterFactory; + this.clock = clock; + this.connectionManager = connectionManager; + this.contactManager = contactManager; + this.crypto = crypto; + this.streamReaderFactory = streamReaderFactory; + this.streamWriterFactory = streamWriterFactory; + } + + @Override + public void startExchange(ContactExchangeListener listener, + LocalAuthor localAuthor, SecretKey masterSecret, + DuplexTransportConnection conn, TransportId transportId, + boolean alice) { + this.listener = listener; + this.localAuthor = localAuthor; + this.conn = conn; + this.transportId = transportId; + this.masterSecret = masterSecret; + this.alice = alice; + start(); + } + + @Override + public void run() { + // Derive the header keys for the transport streams + SecretKey aliceHeaderKey = crypto.deriveHeaderKey(masterSecret, true); + SecretKey bobHeaderKey = crypto.deriveHeaderKey(masterSecret, false); + BdfReader r; + BdfWriter w; + try { + // Create the readers + InputStream streamReader = + streamReaderFactory.createInvitationStreamReader( + conn.getReader().getInputStream(), + alice ? bobHeaderKey : aliceHeaderKey); + r = bdfReaderFactory.createReader(streamReader); + // Create the writers + OutputStream streamWriter = + streamWriterFactory.createInvitationStreamWriter( + conn.getWriter().getOutputStream(), + alice ? aliceHeaderKey : bobHeaderKey); + w = bdfWriterFactory.createWriter(streamWriter); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + listener.contactExchangeFailed(); + tryToClose(conn, true); + return; + } + + // Derive the nonces to be signed + byte[] aliceNonce = crypto.deriveSignatureNonce(masterSecret, true); + byte[] bobNonce = crypto.deriveSignatureNonce(masterSecret, false); + + // Exchange pseudonyms, signed nonces, and timestamps + long localTimestamp = clock.currentTimeMillis(); + Author remoteAuthor; + long remoteTimestamp; + try { + if (alice) { + sendPseudonym(w, aliceNonce); + sendTimestamp(w, localTimestamp); + remoteAuthor = receivePseudonym(r, bobNonce); + remoteTimestamp = receiveTimestamp(r); + } else { + remoteAuthor = receivePseudonym(r, aliceNonce); + remoteTimestamp = receiveTimestamp(r); + sendPseudonym(w, bobNonce); + sendTimestamp(w, localTimestamp); + } + // Close the outgoing stream and expect EOF on the incoming stream + w.close(); + if (!r.eof()) LOG.warning("Unexpected data at end of connection"); + } catch (GeneralSecurityException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + listener.contactExchangeFailed(); + tryToClose(conn, true); + return; + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + listener.contactExchangeFailed(); + tryToClose(conn, true); + return; + } + + // The agreed timestamp is the minimum of the peers' timestamps + long timestamp = Math.min(localTimestamp, remoteTimestamp); + + try { + // Add the contact + ContactId contactId = addContact(remoteAuthor, masterSecret, + timestamp, alice); + // Reuse the connection as a transport connection + connectionManager.manageOutgoingConnection(contactId, transportId, + conn); + // Pseudonym exchange succeeded + LOG.info("Pseudonym exchange succeeded"); + listener.contactExchangeSucceeded(remoteAuthor); + } catch (ContactExistsException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + tryToClose(conn, true); + listener.duplicateContact(remoteAuthor); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + tryToClose(conn, true); + listener.contactExchangeFailed(); + } + } + + private void sendPseudonym(BdfWriter w, byte[] nonce) + throws GeneralSecurityException, IOException { + // Sign the nonce + Signature signature = crypto.getSignature(); + KeyParser keyParser = crypto.getSignatureKeyParser(); + byte[] privateKey = localAuthor.getPrivateKey(); + signature.initSign(keyParser.parsePrivateKey(privateKey)); + signature.update(nonce); + byte[] sig = signature.sign(); + // Write the name, public key and signature + w.writeString(localAuthor.getName()); + w.writeRaw(localAuthor.getPublicKey()); + w.writeRaw(sig); + w.flush(); + LOG.info("Sent pseudonym"); + } + + private Author receivePseudonym(BdfReader r, byte[] nonce) + throws GeneralSecurityException, IOException { + // Read the name, public key and signature + String name = r.readString(MAX_AUTHOR_NAME_LENGTH); + byte[] publicKey = r.readRaw(MAX_PUBLIC_KEY_LENGTH); + byte[] sig = r.readRaw(MAX_SIGNATURE_LENGTH); + LOG.info("Received pseudonym"); + // Verify the signature + Signature signature = crypto.getSignature(); + KeyParser keyParser = crypto.getSignatureKeyParser(); + signature.initVerify(keyParser.parsePublicKey(publicKey)); + signature.update(nonce); + if (!signature.verify(sig)) { + if (LOG.isLoggable(INFO)) + LOG.info("Invalid signature"); + throw new GeneralSecurityException(); + } + return authorFactory.createAuthor(name, publicKey); + } + + private void sendTimestamp(BdfWriter w, long timestamp) + throws IOException { + w.writeLong(timestamp); + w.flush(); + LOG.info("Sent timestamp"); + } + + private long receiveTimestamp(BdfReader r) throws IOException { + long timestamp = r.readLong(); + if (timestamp < 0) throw new FormatException(); + LOG.info("Received timestamp"); + return timestamp; + } + + private ContactId addContact(Author remoteAuthor, SecretKey master, + long timestamp, boolean alice) throws DbException { + // Add the contact to the database + return contactManager.addContact(remoteAuthor, localAuthor.getId(), + master, timestamp, alice, true); + } + + private void tryToClose(DuplexTransportConnection conn, + boolean exception) { + try { + LOG.info("Closing connection"); + conn.getReader().dispose(exception, true); + conn.getWriter().dispose(exception); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } +} diff --git a/briar-core/src/org/briarproject/contact/ContactModule.java b/briar-core/src/org/briarproject/contact/ContactModule.java index bd8e2d3afa05b27aedc9cf3117468995f9fbcf01..fb5c290ce2d3ad0915f57a8e44a7c6d71d4b0aec 100644 --- a/briar-core/src/org/briarproject/contact/ContactModule.java +++ b/briar-core/src/org/briarproject/contact/ContactModule.java @@ -1,8 +1,17 @@ package org.briarproject.contact; +import org.briarproject.api.contact.ContactExchangeTask; import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.lifecycle.LifecycleManager; +import org.briarproject.api.plugins.ConnectionManager; +import org.briarproject.api.system.Clock; +import org.briarproject.api.transport.StreamReaderFactory; +import org.briarproject.api.transport.StreamWriterFactory; import javax.inject.Inject; import javax.inject.Singleton; @@ -25,4 +34,16 @@ public class ContactModule { identityManager.registerRemoveIdentityHook(contactManager); return contactManager; } + + @Provides + ContactExchangeTask provideContactExchangeTask( + AuthorFactory authorFactory, BdfReaderFactory bdfReaderFactory, + BdfWriterFactory bdfWriterFactory, Clock clock, + ConnectionManager connectionManager, ContactManager contactManager, + CryptoComponent crypto, StreamReaderFactory streamReaderFactory, + StreamWriterFactory streamWriterFactory) { + return new ContactExchangeTaskImpl(authorFactory, bdfReaderFactory, + bdfWriterFactory, clock, connectionManager, contactManager, + crypto, streamReaderFactory, streamWriterFactory); + } } diff --git a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java index 2a779f3fe200e40f0d8194af97b9898f0dd75fc5..825c3720f5188500ec89aa7a83fb70096b14270a 100644 --- a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java +++ b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java @@ -63,24 +63,22 @@ class CryptoComponentImpl implements CryptoComponent { return s.getBytes(Charset.forName("US-ASCII")); } - // KDF label for bluetooth master key derivation - private static final byte[] BT_MASTER = ascii("MASTER"); // KDF labels for bluetooth confirmation code derivation private static final byte[] BT_A_CONFIRM = ascii("ALICE_CONFIRMATION_CODE"); private static final byte[] BT_B_CONFIRM = ascii("BOB_CONFIRMATION_CODE"); - // KDF labels for bluetooth invitation stream header key derivation - private static final byte[] BT_A_INVITE = ascii("ALICE_INVITATION_KEY"); - private static final byte[] BT_B_INVITE = ascii("BOB_INVITATION_KEY"); - // KDF labels for bluetooth signature nonce derivation - private static final byte[] BT_A_NONCE = ascii("ALICE_SIGNATURE_NONCE"); - private static final byte[] BT_B_NONCE = ascii("BOB_SIGNATURE_NONCE"); + // KDF labels for contact exchange stream header key derivation + private static final byte[] A_INVITE = ascii("ALICE_INVITATION_KEY"); + private static final byte[] B_INVITE = ascii("BOB_INVITATION_KEY"); + // KDF labels for contact exchange signature nonce derivation + private static final byte[] A_SIG_NONCE = ascii("ALICE_SIGNATURE_NONCE"); + private static final byte[] B_SIG_NONCE = ascii("BOB_SIGNATURE_NONCE"); // Hash label for BQP public key commitment derivation private static final byte[] COMMIT = ascii("COMMIT"); - // Hash label for BQP shared secret derivation + // Hash label for shared secret derivation private static final byte[] SHARED_SECRET = ascii("SHARED_SECRET"); // KDF label for BQP confirmation key derivation private static final byte[] CONFIRMATION_KEY = ascii("CONFIRMATION_KEY"); - // KDF label for BQP master key derivation + // KDF label for master key derivation private static final byte[] MASTER_KEY = ascii("MASTER_KEY"); // KDF labels for tag key derivation private static final byte[] A_TAG = ascii("ALICE_TAG_KEY"); @@ -210,12 +208,14 @@ class CryptoComponentImpl implements CryptoComponent { return ByteUtils.readUint(b, CODE_BITS); } - public SecretKey deriveBTInvitationKey(SecretKey master, boolean alice) { - return new SecretKey(macKdf(master, alice ? BT_A_INVITE : BT_B_INVITE)); + public SecretKey deriveHeaderKey(SecretKey master, + boolean alice) { + return new SecretKey(macKdf(master, alice ? A_INVITE : B_INVITE)); } - public byte[] deriveBTSignatureNonce(SecretKey master, boolean alice) { - return macKdf(master, alice ? BT_A_NONCE : BT_B_NONCE); + public byte[] deriveSignatureNonce(SecretKey master, + boolean alice) { + return macKdf(master, alice ? A_SIG_NONCE : B_SIG_NONCE); } public byte[] deriveKeyCommitment(byte[] publicKey) { @@ -438,29 +438,6 @@ class CryptoComponentImpl implements CryptoComponent { } } - // Key derivation function based on a hash function - see NIST SP 800-56A, - // section 5.8 - private byte[] hashKdf(byte[]... inputs) { - Digest digest = new Blake2sDigest(); - // The output of the hash function must be long enough to use as a key - int hashLength = digest.getDigestSize(); - if (hashLength < SecretKey.LENGTH) throw new IllegalStateException(); - // Calculate the hash over the concatenated length-prefixed inputs - byte[] length = new byte[INT_32_BYTES]; - for (byte[] input : inputs) { - ByteUtils.writeUint32(input.length, length, 0); - digest.update(length, 0, length.length); - digest.update(input, 0, input.length); - } - byte[] hash = new byte[hashLength]; - digest.doFinal(hash, 0); - // The output is the first SecretKey.LENGTH bytes of the hash - if (hash.length == SecretKey.LENGTH) return hash; - byte[] truncated = new byte[SecretKey.LENGTH]; - System.arraycopy(hash, 0, truncated, 0, truncated.length); - return truncated; - } - // Key derivation function based on a pseudo-random function - see // NIST SP 800-108, section 5.1 private byte[] macKdf(SecretKey key, byte[]... inputs) { diff --git a/briar-core/src/org/briarproject/invitation/AliceConnector.java b/briar-core/src/org/briarproject/invitation/AliceConnector.java index 210d173d9e6de69942a56c7a56b834134e1d4867..b2947ece4db90335101bc6cb4d1a82424cbaa8a6 100644 --- a/briar-core/src/org/briarproject/invitation/AliceConnector.java +++ b/briar-core/src/org/briarproject/invitation/AliceConnector.java @@ -125,8 +125,8 @@ class AliceConnector extends Connector { if (LOG.isLoggable(INFO)) LOG.info(pluginName + " confirmation succeeded"); // Derive the header keys - SecretKey aliceHeaderKey = crypto.deriveBTInvitationKey(master, true); - SecretKey bobHeaderKey = crypto.deriveBTInvitationKey(master, false); + SecretKey aliceHeaderKey = crypto.deriveHeaderKey(master, true); + SecretKey bobHeaderKey = crypto.deriveHeaderKey(master, false); // Create the readers InputStream streamReader = streamReaderFactory.createInvitationStreamReader(in, @@ -138,8 +138,8 @@ class AliceConnector extends Connector { aliceHeaderKey); w = bdfWriterFactory.createWriter(streamWriter); // Derive the invitation nonces - byte[] aliceNonce = crypto.deriveBTSignatureNonce(master, true); - byte[] bobNonce = crypto.deriveBTSignatureNonce(master, false); + byte[] aliceNonce = crypto.deriveSignatureNonce(master, true); + byte[] bobNonce = crypto.deriveSignatureNonce(master, false); // Exchange pseudonyms, signed nonces, and timestamps Author remoteAuthor; long remoteTimestamp; diff --git a/briar-core/src/org/briarproject/invitation/BobConnector.java b/briar-core/src/org/briarproject/invitation/BobConnector.java index f2c6e2619673922d15a32db891563e5797f25d9d..1460b953ec1b3ec222ca14fc5e27ebfc93447966 100644 --- a/briar-core/src/org/briarproject/invitation/BobConnector.java +++ b/briar-core/src/org/briarproject/invitation/BobConnector.java @@ -125,8 +125,10 @@ class BobConnector extends Connector { if (LOG.isLoggable(INFO)) LOG.info(pluginName + " confirmation succeeded"); // Derive the header keys - SecretKey aliceHeaderKey = crypto.deriveBTInvitationKey(master, true); - SecretKey bobHeaderKey = crypto.deriveBTInvitationKey(master, false); + SecretKey aliceHeaderKey = crypto.deriveHeaderKey(master, + true); + SecretKey bobHeaderKey = crypto.deriveHeaderKey(master, + false); // Create the readers InputStream streamReader = streamReaderFactory.createInvitationStreamReader(in, @@ -138,8 +140,10 @@ class BobConnector extends Connector { bobHeaderKey); w = bdfWriterFactory.createWriter(streamWriter); // Derive the nonces - byte[] aliceNonce = crypto.deriveBTSignatureNonce(master, true); - byte[] bobNonce = crypto.deriveBTSignatureNonce(master, false); + byte[] aliceNonce = crypto.deriveSignatureNonce(master, + true); + byte[] bobNonce = crypto.deriveSignatureNonce(master, + false); // Exchange pseudonyms, signed nonces and timestamps Author remoteAuthor; long remoteTimestamp; diff --git a/briar-core/src/org/briarproject/keyagreement/AbortException.java b/briar-core/src/org/briarproject/keyagreement/AbortException.java new file mode 100644 index 0000000000000000000000000000000000000000..670bbc3ee14f63d6bd1d543d58d06652576c4437 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/AbortException.java @@ -0,0 +1,23 @@ +package org.briarproject.keyagreement; + +class AbortException extends Exception { + public boolean receivedAbort; + + public AbortException() { + this(false); + } + + public AbortException(boolean receivedAbort) { + super(); + this.receivedAbort = receivedAbort; + } + + public AbortException(Exception e) { + this(e, false); + } + + public AbortException(Exception e, boolean receivedAbort) { + super(e); + this.receivedAbort = receivedAbort; + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java new file mode 100644 index 0000000000000000000000000000000000000000..e87297af63402215582b79de7ad5bd98f63ec31e --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java @@ -0,0 +1,236 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyPair; +import org.briarproject.api.keyagreement.KeyAgreementConnection; +import org.briarproject.api.keyagreement.KeyAgreementListener; +import org.briarproject.api.keyagreement.Payload; +import org.briarproject.api.keyagreement.TransportDescriptor; +import org.briarproject.api.plugins.PluginManager; +import org.briarproject.api.plugins.duplex.DuplexPlugin; +import org.briarproject.api.plugins.duplex.DuplexTransportConnection; +import org.briarproject.api.system.Clock; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.Future; +import java.util.logging.Logger; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT; + +class KeyAgreementConnector { + + interface Callbacks { + void connectionWaiting(); + } + + private static final Logger LOG = + Logger.getLogger(KeyAgreementConnector.class.getName()); + + private final Callbacks callbacks; + private final Clock clock; + private final CryptoComponent crypto; + private final PluginManager pluginManager; + private final CompletionService<KeyAgreementConnection> connect; + + private final List<KeyAgreementListener> listeners = + new ArrayList<KeyAgreementListener>(); + private final List<Future<KeyAgreementConnection>> pending = + new ArrayList<Future<KeyAgreementConnection>>(); + + private volatile boolean connecting = false; + private volatile boolean alice = false; + + public KeyAgreementConnector(Callbacks callbacks, Clock clock, + CryptoComponent crypto, PluginManager pluginManager, + Executor ioExecutor) { + this.callbacks = callbacks; + this.clock = clock; + this.crypto = crypto; + this.pluginManager = pluginManager; + connect = new ExecutorCompletionService<KeyAgreementConnection>( + ioExecutor); + } + + public Payload listen(KeyPair localKeyPair) { + LOG.info("Starting BQP listeners"); + // Derive commitment + byte[] commitment = crypto.deriveKeyCommitment( + localKeyPair.getPublic().getEncoded()); + // Start all listeners and collect their descriptors + List<TransportDescriptor> descriptors = + new ArrayList<TransportDescriptor>(); + for (DuplexPlugin plugin : pluginManager.getKeyAgreementPlugins()) { + KeyAgreementListener l = plugin.createKeyAgreementListener( + commitment); + if (l != null) { + TransportDescriptor d = l.getDescriptor(); + descriptors.add(d); + pending.add(connect.submit(new ReadableTask(l.listen()))); + listeners.add(l); + } + } + return new Payload(commitment, descriptors); + } + + public void stopListening() { + LOG.info("Stopping BQP listeners"); + for (KeyAgreementListener l : listeners) { + l.close(); + } + listeners.clear(); + } + + public KeyAgreementTransport connect(Payload remotePayload, + boolean alice) { + // Let the listeners know if we are Alice + this.connecting = true; + this.alice = alice; + long end = clock.currentTimeMillis() + CONNECTION_TIMEOUT; + + // Start connecting over supported transports + LOG.info("Starting outgoing BQP connections"); + for (TransportDescriptor d : remotePayload.getTransportDescriptors()) { + DuplexPlugin plugin = (DuplexPlugin) pluginManager.getPlugin( + d.getIdentifier()); + if (plugin != null) + pending.add(connect.submit(new ReadableTask( + new ConnectorTask(plugin, remotePayload.getCommitment(), + d, end)))); + } + + // Get chosen connection + KeyAgreementConnection chosen = null; + try { + long now = clock.currentTimeMillis(); + Future<KeyAgreementConnection> f = + connect.poll(end - now, MILLISECONDS); + if (f == null) + return null; // No task completed within the timeout. + chosen = f.get(); + return new KeyAgreementTransport(chosen); + } catch (InterruptedException e) { + LOG.info("Interrupted while waiting for connection"); + Thread.currentThread().interrupt(); + return null; + } catch (ExecutionException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } finally { + stopListening(); + // Close all other connections + closePending(chosen); + } + } + + private void closePending(KeyAgreementConnection chosen) { + for (Future<KeyAgreementConnection> f : pending) { + try { + if (f.cancel(true)) + LOG.info("Cancelled task"); + else if (!f.isCancelled()) { + KeyAgreementConnection c = f.get(); + if (c != null && c != chosen) + tryToClose(c.getConnection(), false); + } + } catch (InterruptedException e) { + LOG.info("Interrupted while closing sockets"); + Thread.currentThread().interrupt(); + return; + } catch (ExecutionException e) { + if (LOG.isLoggable(INFO)) LOG.info(e.toString()); + } + } + } + + private void tryToClose(DuplexTransportConnection conn, boolean exception) { + try { + if (LOG.isLoggable(INFO)) + LOG.info("Closing connection, exception: " + exception); + conn.getReader().dispose(exception, true); + conn.getWriter().dispose(exception); + } catch (IOException e) { + if (LOG.isLoggable(INFO)) LOG.info(e.toString()); + } + } + + private class ConnectorTask implements Callable<KeyAgreementConnection> { + + private final byte[] commitment; + private final TransportDescriptor descriptor; + private final long end; + private final DuplexPlugin plugin; + + private ConnectorTask(DuplexPlugin plugin, byte[] commitment, + TransportDescriptor descriptor, long end) { + this.plugin = plugin; + this.commitment = commitment; + this.descriptor = descriptor; + this.end = end; + } + + @Override + public KeyAgreementConnection call() throws Exception { + // Repeat attempts until we connect or get interrupted + while (true) { + long now = clock.currentTimeMillis(); + DuplexTransportConnection conn = + plugin.createKeyAgreementConnection(commitment, + descriptor, end - now); + if (conn != null) { + if (LOG.isLoggable(INFO)) + LOG.info(plugin.getId().getString() + + ": Outgoing connection"); + return new KeyAgreementConnection(conn, plugin.getId()); + } + // Wait 2s before retry (to circumvent transient failures) + Thread.sleep(2000); + } + } + } + + private class ReadableTask + implements Callable<KeyAgreementConnection> { + + private final Callable<KeyAgreementConnection> connectionTask; + + private ReadableTask(Callable<KeyAgreementConnection> connectionTask) { + this.connectionTask = connectionTask; + } + + @Override + public KeyAgreementConnection call() + throws Exception { + KeyAgreementConnection c = connectionTask.call(); + InputStream in = c.getConnection().getReader().getInputStream(); + boolean waitingSent = false; + while (!alice && in.available() == 0) { + if (!waitingSent && connecting && !alice) { + // Bob waits here until Alice obtains his payload. + callbacks.connectionWaiting(); + waitingSent = true; + } + if (LOG.isLoggable(INFO)) + LOG.info(c.getTransportId().toString() + + ": Waiting for connection"); + Thread.sleep(1000); + } + if (!alice && LOG.isLoggable(INFO)) + LOG.info(c.getTransportId().toString() + ": Data available"); + return c; + } + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementModule.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementModule.java new file mode 100644 index 0000000000000000000000000000000000000000..9f6a529028a668fc4cbde1231a3a61201730f847 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementModule.java @@ -0,0 +1,43 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.keyagreement.KeyAgreementTaskFactory; +import org.briarproject.api.keyagreement.PayloadEncoder; +import org.briarproject.api.keyagreement.PayloadParser; +import org.briarproject.api.lifecycle.IoExecutor; +import org.briarproject.api.plugins.PluginManager; +import org.briarproject.api.system.Clock; + +import java.util.concurrent.Executor; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class KeyAgreementModule { + + @Provides + @Singleton + KeyAgreementTaskFactory provideKeyAgreementTaskFactory(Clock clock, + CryptoComponent crypto, EventBus eventBus, + @IoExecutor Executor ioExecutor, PayloadEncoder payloadEncoder, + PluginManager pluginManager) { + return new KeyAgreementTaskFactoryImpl(clock, crypto, eventBus, + ioExecutor, payloadEncoder, pluginManager); + } + + @Provides + PayloadEncoder providePayloadEncoder(BdfWriterFactory bdfWriterFactory) { + return new PayloadEncoderImpl(bdfWriterFactory); + } + + @Provides + PayloadParser providePayloadParser(BdfReaderFactory bdfReaderFactory) { + return new PayloadParserImpl(bdfReaderFactory); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementProtocol.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..c832edd092e10c3227eeed2e538c7ed25b901f88 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementProtocol.java @@ -0,0 +1,157 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyPair; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.keyagreement.Payload; +import org.briarproject.api.keyagreement.PayloadEncoder; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +/** + * Implementation of the BQP protocol. + * <p/> + * Alice: + * <ul> + * <li>Send A_KEY</li> + * <li>Receive B_KEY + * <ul> + * <li>Check B_KEY matches B_COMMIT</li> + * </ul></li> + * <li>Calculate s</li> + * <li>Send A_CONFIRM</li> + * <li>Receive B_CONFIRM + * <ul> + * <li>Check B_CONFIRM matches expected</li> + * </ul></li> + * <li>Derive master</li> + * </ul><p/> + * Bob: + * <ul> + * <li>Receive A_KEY + * <ul> + * <li>Check A_KEY matches A_COMMIT</li> + * </ul></li> + * <li>Send B_KEY</li> + * <li>Calculate s</li> + * <li>Receive A_CONFIRM + * <ul> + * <li>Check A_CONFIRM matches expected</li> + * </ul></li> + * <li>Send B_CONFIRM</li> + * <li>Derive master</li> + * </ul> + */ +class KeyAgreementProtocol { + + interface Callbacks { + void connectionWaiting(); + void initialPacketReceived(); + } + + private Callbacks callbacks; + private CryptoComponent crypto; + private PayloadEncoder payloadEncoder; + private KeyAgreementTransport transport; + private Payload theirPayload, ourPayload; + private KeyPair ourKeyPair; + private boolean alice; + + public KeyAgreementProtocol(Callbacks callbacks, CryptoComponent crypto, + PayloadEncoder payloadEncoder, KeyAgreementTransport transport, + Payload theirPayload, Payload ourPayload, KeyPair ourKeyPair, + boolean alice) { + this.callbacks = callbacks; + this.crypto = crypto; + this.payloadEncoder = payloadEncoder; + this.transport = transport; + this.theirPayload = theirPayload; + this.ourPayload = ourPayload; + this.ourKeyPair = ourKeyPair; + this.alice = alice; + } + + /** + * Perform the BQP protocol. + * + * @return the negotiated master secret. + * @throws AbortException when the protocol may have been tampered with. + * @throws IOException for all other other connection errors. + */ + public SecretKey perform() throws AbortException, IOException { + try { + byte[] theirPublicKey; + if (alice) { + sendKey(); + // Alice waits here until Bob obtains her payload. + callbacks.connectionWaiting(); + theirPublicKey = receiveKey(); + } else { + theirPublicKey = receiveKey(); + sendKey(); + } + SecretKey s = deriveSharedSecret(theirPublicKey); + if (alice) { + sendConfirm(s, theirPublicKey); + receiveConfirm(s, theirPublicKey); + } else { + receiveConfirm(s, theirPublicKey); + sendConfirm(s, theirPublicKey); + } + return crypto.deriveMasterSecret(s); + } catch (AbortException e) { + sendAbort(e.getCause() != null); + throw e; + } + } + + private void sendKey() throws IOException { + transport.sendKey(ourKeyPair.getPublic().getEncoded()); + } + + private byte[] receiveKey() throws AbortException { + byte[] publicKey = transport.receiveKey(); + callbacks.initialPacketReceived(); + byte[] expected = crypto.deriveKeyCommitment(publicKey); + if (!Arrays.equals(expected, theirPayload.getCommitment())) + throw new AbortException(); + return publicKey; + } + + private SecretKey deriveSharedSecret(byte[] theirPublicKey) + throws AbortException { + try { + return crypto.deriveSharedSecret(theirPublicKey, ourKeyPair, alice); + } catch (GeneralSecurityException e) { + throw new AbortException(e); + } + } + + private void sendConfirm(SecretKey s, byte[] theirPublicKey) + throws IOException { + byte[] confirm = crypto.deriveConfirmationRecord(s, + payloadEncoder.encode(theirPayload), + payloadEncoder.encode(ourPayload), + theirPublicKey, ourKeyPair, + alice, alice); + transport.sendConfirm(confirm); + } + + private void receiveConfirm(SecretKey s, byte[] theirPublicKey) + throws AbortException { + byte[] confirm = transport.receiveConfirm(); + byte[] expected = crypto.deriveConfirmationRecord(s, + payloadEncoder.encode(theirPayload), + payloadEncoder.encode(ourPayload), + theirPublicKey, ourKeyPair, + alice, !alice); + if (!Arrays.equals(expected, confirm)) + throw new AbortException(); + } + + private void sendAbort(boolean exception) { + transport.sendAbort(exception); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskFactoryImpl.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..aafde49b3b1acb32aa01c995b9402a96cf615e41 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskFactoryImpl.java @@ -0,0 +1,46 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.KeyAgreementAbortedEvent; +import org.briarproject.api.event.KeyAgreementFailedEvent; +import org.briarproject.api.event.KeyAgreementFinishedEvent; +import org.briarproject.api.keyagreement.KeyAgreementTask; +import org.briarproject.api.keyagreement.KeyAgreementTaskFactory; +import org.briarproject.api.keyagreement.PayloadEncoder; +import org.briarproject.api.lifecycle.IoExecutor; +import org.briarproject.api.plugins.PluginManager; +import org.briarproject.api.system.Clock; + +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +class KeyAgreementTaskFactoryImpl implements KeyAgreementTaskFactory { + + private final Clock clock; + private final CryptoComponent crypto; + private final EventBus eventBus; + private final Executor ioExecutor; + private final PayloadEncoder payloadEncoder; + private final PluginManager pluginManager; + + @Inject + KeyAgreementTaskFactoryImpl(Clock clock, CryptoComponent crypto, + EventBus eventBus, @IoExecutor Executor ioExecutor, + PayloadEncoder payloadEncoder, PluginManager pluginManager) { + this.clock = clock; + this.crypto = crypto; + this.eventBus = eventBus; + this.ioExecutor = ioExecutor; + this.payloadEncoder = payloadEncoder; + this.pluginManager = pluginManager; + } + + public KeyAgreementTask getTask() { + return new KeyAgreementTaskImpl(clock, crypto, eventBus, payloadEncoder, + pluginManager, ioExecutor); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskImpl.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..12f7678740421fbae7f1251c7f7a107cb6b967f8 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskImpl.java @@ -0,0 +1,135 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyPair; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.event.KeyAgreementAbortedEvent; +import org.briarproject.api.event.KeyAgreementFailedEvent; +import org.briarproject.api.event.KeyAgreementFinishedEvent; +import org.briarproject.api.event.KeyAgreementListeningEvent; +import org.briarproject.api.event.KeyAgreementStartedEvent; +import org.briarproject.api.event.KeyAgreementWaitingEvent; +import org.briarproject.api.keyagreement.KeyAgreementResult; +import org.briarproject.api.keyagreement.KeyAgreementTask; +import org.briarproject.api.keyagreement.Payload; +import org.briarproject.api.keyagreement.PayloadEncoder; +import org.briarproject.api.plugins.PluginManager; +import org.briarproject.api.system.Clock; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; + +class KeyAgreementTaskImpl extends Thread implements + KeyAgreementTask, KeyAgreementConnector.Callbacks, + KeyAgreementProtocol.Callbacks { + + private static final Logger LOG = + Logger.getLogger(KeyAgreementTaskImpl.class.getName()); + + private final CryptoComponent crypto; + private final EventBus eventBus; + private final PayloadEncoder payloadEncoder; + private final KeyPair localKeyPair; + private final KeyAgreementConnector connector; + + private Payload localPayload; + private Payload remotePayload; + + public KeyAgreementTaskImpl(Clock clock, CryptoComponent crypto, + EventBus eventBus, PayloadEncoder payloadEncoder, + PluginManager pluginManager, Executor ioExecutor) { + this.crypto = crypto; + this.eventBus = eventBus; + this.payloadEncoder = payloadEncoder; + localKeyPair = crypto.generateAgreementKeyPair(); + connector = new KeyAgreementConnector(this, clock, crypto, + pluginManager, ioExecutor); + } + + @Override + public synchronized void listen() { + if (localPayload == null) { + localPayload = connector.listen(localKeyPair); + eventBus.broadcast(new KeyAgreementListeningEvent(localPayload)); + } + } + + @Override + public synchronized void stopListening() { + if (localPayload != null) { + if (remotePayload == null) + connector.stopListening(); + else + interrupt(); + } + } + + @Override + public synchronized void connectAndRunProtocol(Payload remotePayload) { + if (this.localPayload == null) + throw new IllegalStateException( + "Must listen before connecting"); + if (this.remotePayload != null) + throw new IllegalStateException( + "Already provided remote payload for this task"); + this.remotePayload = remotePayload; + start(); + } + + @Override + public void run() { + boolean alice = localPayload.compareTo(remotePayload) < 0; + + // Open connection to remote device + KeyAgreementTransport transport = + connector.connect(remotePayload, alice); + if (transport == null) { + // Notify caller that the connection failed + eventBus.broadcast(new KeyAgreementFailedEvent()); + return; + } + + // Run BQP protocol over the connection + LOG.info("Starting BQP protocol"); + KeyAgreementProtocol protocol = new KeyAgreementProtocol(this, crypto, + payloadEncoder, transport, remotePayload, localPayload, + localKeyPair, alice); + try { + SecretKey master = protocol.perform(); + KeyAgreementResult result = + new KeyAgreementResult(master, transport.getConnection(), + transport.getTransportId(), alice); + LOG.info("Finished BQP protocol"); + // Broadcast result to caller + eventBus.broadcast(new KeyAgreementFinishedEvent(result)); + } catch (AbortException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + // Notify caller that the protocol was aborted + eventBus.broadcast(new KeyAgreementAbortedEvent(e.receivedAbort)); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + // Notify caller that the connection failed + eventBus.broadcast(new KeyAgreementFailedEvent()); + } + } + + @Override + public void connectionWaiting() { + eventBus.broadcast(new KeyAgreementWaitingEvent()); + } + + @Override + public void initialPacketReceived() { + // We send this here instead of when we create the protocol, so that + // if device A makes a connection after getting device B's payload and + // starts its protocol, device A's UI doesn't change to prevent device B + // from getting device A's payload. + eventBus.broadcast(new KeyAgreementStartedEvent()); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementTransport.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTransport.java new file mode 100644 index 0000000000000000000000000000000000000000..25a18962bad176a7d58535d2fd5009d6977b1a51 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTransport.java @@ -0,0 +1,131 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.TransportId; +import org.briarproject.api.keyagreement.KeyAgreementConnection; +import org.briarproject.api.plugins.duplex.DuplexTransportConnection; +import org.briarproject.util.ByteUtils; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION; +import static org.briarproject.api.keyagreement.KeyAgreementConstants.RECORD_HEADER_LENGTH; +import static org.briarproject.api.keyagreement.KeyAgreementConstants.RECORD_HEADER_PAYLOAD_LENGTH_OFFSET; +import static org.briarproject.api.keyagreement.RecordTypes.ABORT; +import static org.briarproject.api.keyagreement.RecordTypes.CONFIRM; +import static org.briarproject.api.keyagreement.RecordTypes.KEY; + +/** + * Handles the sending and receiving of BQP records. + */ +class KeyAgreementTransport { + + private static final Logger LOG = + Logger.getLogger(KeyAgreementTransport.class.getName()); + + private final KeyAgreementConnection kac; + private final InputStream in; + private final OutputStream out; + + public KeyAgreementTransport(KeyAgreementConnection kac) + throws IOException { + this.kac = kac; + in = kac.getConnection().getReader().getInputStream(); + out = kac.getConnection().getWriter().getOutputStream(); + } + + public DuplexTransportConnection getConnection() { + return kac.getConnection(); + } + + public TransportId getTransportId() { + return kac.getTransportId(); + } + + public void sendKey(byte[] key) throws IOException { + writeRecord(KEY, key); + } + + public byte[] receiveKey() throws AbortException { + return readRecord(KEY); + } + + public void sendConfirm(byte[] confirm) throws IOException { + writeRecord(CONFIRM, confirm); + } + + public byte[] receiveConfirm() throws AbortException { + return readRecord(CONFIRM); + } + + public void sendAbort(boolean exception) { + try { + writeRecord(ABORT, new byte[0]); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + exception = true; + } + tryToClose(exception); + } + + public void tryToClose(boolean exception) { + try { + LOG.info("Closing connection"); + kac.getConnection().getReader().dispose(exception, true); + kac.getConnection().getWriter().dispose(exception); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + private void writeRecord(byte type, byte[] payload) throws IOException { + byte[] recordHeader = new byte[RECORD_HEADER_LENGTH]; + recordHeader[0] = PROTOCOL_VERSION; + recordHeader[1] = type; + ByteUtils.writeUint16(payload.length, recordHeader, + RECORD_HEADER_PAYLOAD_LENGTH_OFFSET); + out.write(recordHeader); + out.write(payload); + out.flush(); + } + + private byte[] readRecord(byte type) throws AbortException { + byte[] header = readHeader(); + if (header[0] != PROTOCOL_VERSION) + throw new AbortException(); // TODO handle? + if (header[1] != type) { + // Unexpected packet + throw new AbortException(header[1] == ABORT); + } + int len = ByteUtils.readUint16(header, + RECORD_HEADER_PAYLOAD_LENGTH_OFFSET); + try { + return readData(len); + } catch (IOException e) { + throw new AbortException(e); + } + } + + private byte[] readHeader() throws AbortException { + try { + return readData(RECORD_HEADER_LENGTH); + } catch (IOException e) { + throw new AbortException(e); + } + } + + private byte[] readData(int len) throws IOException { + byte[] data = new byte[len]; + int offset = 0; + while (offset < data.length) { + int read = in.read(data, offset, data.length - offset); + if (read == -1) throw new EOFException(); + offset += read; + } + return data; + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java b/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..8a26f9405bd17128dc7cfde4ca89bb90859f13d0 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java @@ -0,0 +1,48 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.data.BdfWriter; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.keyagreement.Payload; +import org.briarproject.api.keyagreement.PayloadEncoder; +import org.briarproject.api.keyagreement.TransportDescriptor; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.inject.Inject; + +import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION; + +class PayloadEncoderImpl implements PayloadEncoder { + + private final BdfWriterFactory bdfWriterFactory; + + @Inject + public PayloadEncoderImpl(BdfWriterFactory bdfWriterFactory) { + this.bdfWriterFactory = bdfWriterFactory; + } + + @Override + public byte[] encode(Payload p) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + try { + w.writeListStart(); // Payload start + w.writeLong(PROTOCOL_VERSION); + w.writeRaw(p.getCommitment()); + w.writeListStart(); // Descriptors start + for (TransportDescriptor d : p.getTransportDescriptors()) { + w.writeListStart(); + w.writeString(d.getIdentifier().getString()); + w.writeDictionary(d.getProperties()); + w.writeListEnd(); + } + w.writeListEnd(); // Descriptors end + w.writeListEnd(); // Payload end + } catch (IOException e) { + // Shouldn't happen with ByteArrayOutputStream + throw new RuntimeException(e); + } + return out.toByteArray(); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java b/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d13f9ff73228af96b26a814a6d55e19a3e4d9a2b --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java @@ -0,0 +1,68 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.FormatException; +import org.briarproject.api.TransportId; +import org.briarproject.api.data.BdfReader; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.keyagreement.Payload; +import org.briarproject.api.keyagreement.PayloadParser; +import org.briarproject.api.keyagreement.TransportDescriptor; +import org.briarproject.api.properties.TransportProperties; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import static org.briarproject.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH; +import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION; +import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH; + +class PayloadParserImpl implements PayloadParser { + + private final BdfReaderFactory bdfReaderFactory; + + @Inject + public PayloadParserImpl(BdfReaderFactory bdfReaderFactory) { + this.bdfReaderFactory = bdfReaderFactory; + } + + @Override + public Payload parse(byte[] raw) throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(raw); + BdfReader r = bdfReaderFactory.createReader(in); + r.readListStart(); // Payload start + int proto = (int) r.readLong(); + if (proto != PROTOCOL_VERSION) + throw new FormatException(); + byte[] commitment = r.readRaw(COMMIT_LENGTH); + if (commitment.length != COMMIT_LENGTH) + throw new FormatException(); + List<TransportDescriptor> descriptors = new ArrayList<TransportDescriptor>(); + r.readListStart(); // Descriptors start + while (r.hasList()) { + r.readListStart(); + while (!r.hasListEnd()) { + TransportId id = + new TransportId(r.readString(MAX_PROPERTY_LENGTH)); + TransportProperties p = new TransportProperties(); + r.readDictionaryStart(); + while (!r.hasDictionaryEnd()) { + String key = r.readString(MAX_PROPERTY_LENGTH); + String value = r.readString(MAX_PROPERTY_LENGTH); + p.put(key, value); + } + r.readDictionaryEnd(); + descriptors.add(new TransportDescriptor(id, p)); + } + r.readListEnd(); + } + r.readListEnd(); // Descriptors end + r.readListEnd(); // Payload end + if (!r.eof()) + throw new FormatException(); + return new Payload(commitment, descriptors); + } +} diff --git a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java index 367b1b66c5be99be31d6e20c930f5702bab09893..ef6e65e19d94ceb28a69c84f746e8d943a51718e 100644 --- a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java +++ b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java @@ -149,6 +149,13 @@ class PluginManagerImpl implements PluginManager, Service { return Collections.unmodifiableList(supported); } + public Collection<DuplexPlugin> getKeyAgreementPlugins() { + List<DuplexPlugin> supported = new ArrayList<DuplexPlugin>(); + for (DuplexPlugin d : duplexPlugins) + if (d.supportsKeyAgreement()) supported.add(d); + return Collections.unmodifiableList(supported); + } + private class SimplexPluginStarter implements Runnable { private final SimplexPluginFactory factory; diff --git a/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java b/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java index 54376310a0c33bd0b4e4f665f503d1c1ca97b76e..d6effe2a722930d94a5a6dac0022e02005ecb30c 100644 --- a/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java +++ b/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java @@ -2,6 +2,8 @@ package org.briarproject.plugins.tcp; import org.briarproject.api.contact.ContactId; import org.briarproject.api.crypto.PseudoRandom; +import org.briarproject.api.keyagreement.KeyAgreementListener; +import org.briarproject.api.keyagreement.TransportDescriptor; import org.briarproject.api.plugins.Backoff; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; @@ -250,6 +252,20 @@ abstract class TcpPlugin implements DuplexPlugin { throw new UnsupportedOperationException(); } + public boolean supportsKeyAgreement() { + return false; + } + + public KeyAgreementListener createKeyAgreementListener( + byte[] commitment) { + throw new UnsupportedOperationException(); + } + + public DuplexTransportConnection createKeyAgreementConnection( + byte[] commitment, TransportDescriptor d, long timeout) { + throw new UnsupportedOperationException(); + } + protected Collection<InetAddress> getLocalIpAddresses() { List<NetworkInterface> ifaces; try { diff --git a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java index 79e289c39ca3dd1b538f65f6e037a39a97e21f0f..875112c5e88abe0baed8035d2a9d0da319000ec8 100644 --- a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java +++ b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java @@ -3,6 +3,9 @@ package org.briarproject.plugins.bluetooth; import org.briarproject.api.TransportId; import org.briarproject.api.contact.ContactId; import org.briarproject.api.crypto.PseudoRandom; +import org.briarproject.api.keyagreement.KeyAgreementConnection; +import org.briarproject.api.keyagreement.KeyAgreementListener; +import org.briarproject.api.keyagreement.TransportDescriptor; import org.briarproject.api.plugins.Backoff; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; @@ -50,6 +53,9 @@ class BluetoothPlugin implements DuplexPlugin { Logger.getLogger(BluetoothPlugin.class.getName()); private static final int UUID_BYTES = 16; + private static final String PROP_ADDRESS = "address"; + private static final String PROP_UUID = "uuid"; + private final Executor ioExecutor; private final SecureRandom secureRandom; private final Backoff backoff; @@ -106,7 +112,7 @@ class BluetoothPlugin implements DuplexPlugin { if (!running) return; // Advertise the Bluetooth address to contacts TransportProperties p = new TransportProperties(); - p.put("address", localDevice.getBluetoothAddress()); + p.put(PROP_ADDRESS, localDevice.getBluetoothAddress()); callback.mergeLocalProperties(p); // Bind a server socket to accept connections from contacts String url = makeUrl("localhost", getUuid()); @@ -135,13 +141,13 @@ class BluetoothPlugin implements DuplexPlugin { } private String getUuid() { - String uuid = callback.getLocalProperties().get("uuid"); + String uuid = callback.getLocalProperties().get(PROP_UUID); if (uuid == null) { byte[] random = new byte[UUID_BYTES]; secureRandom.nextBytes(random); uuid = UUID.nameUUIDFromBytes(random).toString(); TransportProperties p = new TransportProperties(); - p.put("uuid", uuid); + p.put(PROP_UUID, uuid); callback.mergeLocalProperties(p); } return uuid; @@ -203,9 +209,9 @@ class BluetoothPlugin implements DuplexPlugin { for (Entry<ContactId, TransportProperties> e : remote.entrySet()) { final ContactId c = e.getKey(); if (connected.contains(c)) continue; - final String address = e.getValue().get("address"); + final String address = e.getValue().get(PROP_ADDRESS); if (StringUtils.isNullOrEmpty(address)) continue; - final String uuid = e.getValue().get("uuid"); + final String uuid = e.getValue().get(PROP_UUID); if (StringUtils.isNullOrEmpty(uuid)) continue; ioExecutor.execute(new Runnable() { public void run() { @@ -236,9 +242,9 @@ class BluetoothPlugin implements DuplexPlugin { if (!running) return null; TransportProperties p = callback.getRemoteProperties().get(c); if (p == null) return null; - String address = p.get("address"); + String address = p.get(PROP_ADDRESS); if (StringUtils.isNullOrEmpty(address)) return null; - String uuid = p.get("uuid"); + String uuid = p.get(PROP_UUID); if (StringUtils.isNullOrEmpty(uuid)) return null; String url = makeUrl(address, uuid); StreamConnection s = connect(url); @@ -335,6 +341,54 @@ class BluetoothPlugin implements DuplexPlugin { }); } + public boolean supportsKeyAgreement() { + return true; + } + + public KeyAgreementListener createKeyAgreementListener( + byte[] localCommitment) { + // No truncation necessary because COMMIT_LENGTH = 16 + String uuid = UUID.nameUUIDFromBytes(localCommitment).toString(); + if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid); + String url = makeUrl("localhost", uuid); + // Make the device discoverable if possible + makeDeviceDiscoverable(); + // Bind a server socket for receiving invitation connections + final StreamConnectionNotifier ss; + try { + ss = (StreamConnectionNotifier) Connector.open(url); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } + if (!running) { + tryToClose(ss); + return null; + } + TransportProperties p = new TransportProperties(); + p.put(PROP_ADDRESS, localDevice.getBluetoothAddress()); + TransportDescriptor d = new TransportDescriptor(ID, p); + return new BluetoothKeyAgreementListener(d, ss); + } + + public DuplexTransportConnection createKeyAgreementConnection( + byte[] remoteCommitment, TransportDescriptor d, long timeout) { + if (!isRunning()) return null; + if (!ID.equals(d.getIdentifier())) return null; + TransportProperties p = d.getProperties(); + if (p == null) return null; + String address = p.get(PROP_ADDRESS); + if (StringUtils.isNullOrEmpty(address)) return null; + // No truncation necessary because COMMIT_LENGTH = 16 + String uuid = UUID.nameUUIDFromBytes(remoteCommitment).toString(); + if (LOG.isLoggable(INFO)) + LOG.info("Connecting to key agreement UUID " + uuid); + String url = makeUrl(address, uuid); + StreamConnection s = connect(url); + if (s == null) return null; + return new BluetoothTransportConnection(this, s); + } + private void makeDeviceDiscoverable() { // Try to make the device discoverable (requires root on Linux) try { @@ -414,4 +468,39 @@ class BluetoothPlugin implements DuplexPlugin { return s; } } + + private class BluetoothKeyAgreementListener extends KeyAgreementListener { + + private final StreamConnectionNotifier ss; + + public BluetoothKeyAgreementListener(TransportDescriptor descriptor, + StreamConnectionNotifier ss) { + super(descriptor); + this.ss = ss; + } + + @Override + public Callable<KeyAgreementConnection> listen() { + return new Callable<KeyAgreementConnection>() { + @Override + public KeyAgreementConnection call() throws Exception { + StreamConnection s = ss.acceptAndOpen(); + if (LOG.isLoggable(INFO)) + LOG.info(ID.getString() + ": Incoming connection"); + return new KeyAgreementConnection( + new BluetoothTransportConnection( + BluetoothPlugin.this, s), ID); + } + }; + } + + @Override + public void close() { + try { + ss.close(); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + } } diff --git a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java index ffa06c2a477689045735b9c1aa5cd28ebd04ae07..bbf71da9c0cafe5e92c2ce2f1d056ad29b6a20f3 100644 --- a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java +++ b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java @@ -3,6 +3,8 @@ package org.briarproject.plugins.modem; import org.briarproject.api.TransportId; import org.briarproject.api.contact.ContactId; import org.briarproject.api.crypto.PseudoRandom; +import org.briarproject.api.keyagreement.KeyAgreementListener; +import org.briarproject.api.keyagreement.TransportDescriptor; import org.briarproject.api.plugins.TransportConnectionReader; import org.briarproject.api.plugins.TransportConnectionWriter; import org.briarproject.api.plugins.duplex.DuplexPlugin; @@ -158,6 +160,20 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { throw new UnsupportedOperationException(); } + public boolean supportsKeyAgreement() { + return false; + } + + public KeyAgreementListener createKeyAgreementListener( + byte[] commitment) { + throw new UnsupportedOperationException(); + } + + public DuplexTransportConnection createKeyAgreementConnection( + byte[] commitment, TransportDescriptor d, long timeout) { + throw new UnsupportedOperationException(); + } + public void incomingCallConnected() { LOG.info("Incoming call connected"); callback.incomingConnectionCreated(new ModemTransportConnection()); diff --git a/briar-tests/build.gradle b/briar-tests/build.gradle index c5ae112372d62d09b74d1e6eb28c406c1ef32bf5..b3c2af66e1cda3509e1d645e778b820a55f89c92 100644 --- a/briar-tests/build.gradle +++ b/briar-tests/build.gradle @@ -15,6 +15,8 @@ dependencies { compile project(':briar-desktop') compile "junit:junit:4.12" compile "org.jmock:jmock:2.8.1" + compile "org.jmock:jmock-junit4:2.8.1" + compile "org.jmock:jmock-legacy:2.8.1" compile "org.hamcrest:hamcrest-library:1.3" compile "org.hamcrest:hamcrest-core:1.3" } @@ -23,6 +25,8 @@ dependencyVerification { verify = [ 'junit:junit:59721f0805e223d84b90677887d9ff567dc534d7c502ca903c0c2b17f05c116a', 'org.jmock:jmock:75d4bdaf636879f0215830c5e6ab99407069a625eaffde5d57b32d887b75dc14', + 'org.jmock:jmock-junit4:81e3fff46ed56738a6f3f5147525d1d85cda591ce5df007cc193e735cee31113', + 'org.jmock:jmock-legacy:19c76059eb254775ba884fc8039bc5c7d1700dc68cc55ad3be5b405a2a8a1819', 'org.hamcrest:hamcrest-library:711d64522f9ec410983bd310934296da134be4254a125080a0416ec178dfad1c', 'org.hamcrest:hamcrest-core:66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9', ] diff --git a/briar-tests/src/org/briarproject/keyagreement/KeyAgreementProtocolTest.java b/briar-tests/src/org/briarproject/keyagreement/KeyAgreementProtocolTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e404c547f48d009081a97493a4422f8cd0d9d200 --- /dev/null +++ b/briar-tests/src/org/briarproject/keyagreement/KeyAgreementProtocolTest.java @@ -0,0 +1,394 @@ +package org.briarproject.keyagreement; + +import org.briarproject.BriarTestCase; +import org.briarproject.TestUtils; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyPair; +import org.briarproject.api.crypto.PublicKey; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.keyagreement.Payload; +import org.briarproject.api.keyagreement.PayloadEncoder; +import org.briarproject.util.StringUtils; +import org.jmock.Expectations; +import org.jmock.auto.Mock; +import org.jmock.integration.junit4.JUnitRuleMockery; +import org.jmock.lib.legacy.ClassImposteriser; +import org.junit.Rule; +import org.junit.Test; + +import static org.briarproject.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class KeyAgreementProtocolTest extends BriarTestCase { + + @Rule + public JUnitRuleMockery context = new JUnitRuleMockery() {{ + // So we can mock concrete classes like KeyAgreementTransport + setImposteriser(ClassImposteriser.INSTANCE); + }}; + + private static final byte[] ALICE_PUBKEY = TestUtils.getRandomBytes(32); + private static final byte[] ALICE_COMMIT = + TestUtils.getRandomBytes(COMMIT_LENGTH); + private static final byte[] ALICE_PAYLOAD = + TestUtils.getRandomBytes(COMMIT_LENGTH + 8); + + private static final byte[] BOB_PUBKEY = TestUtils.getRandomBytes(32); + private static final byte[] BOB_COMMIT = + TestUtils.getRandomBytes(COMMIT_LENGTH); + private static final byte[] BOB_PAYLOAD = + TestUtils.getRandomBytes(COMMIT_LENGTH + 19); + + private static final byte[] ALICE_CONFIRM = + TestUtils.getRandomBytes(SecretKey.LENGTH); + private static final byte[] BOB_CONFIRM = + TestUtils.getRandomBytes(SecretKey.LENGTH); + + private static final byte[] BAD_PUBKEY = TestUtils.getRandomBytes(32); + private static final byte[] BAD_COMMIT = + TestUtils.getRandomBytes(COMMIT_LENGTH); + private static final byte[] BAD_CONFIRM = + TestUtils.getRandomBytes(SecretKey.LENGTH); + + @Mock + KeyAgreementProtocol.Callbacks callbacks; + @Mock + CryptoComponent crypto; + @Mock + PayloadEncoder payloadEncoder; + @Mock + KeyAgreementTransport transport; + @Mock + PublicKey ourPubKey; + + @Test + public void testAliceProtocol() throws Exception { + // set up + final Payload theirPayload = new Payload(BOB_COMMIT, null); + final Payload ourPayload = new Payload(ALICE_COMMIT, null); + final KeyPair ourKeyPair = new KeyPair(ourPubKey, null); + final SecretKey sharedSecret = TestUtils.createSecretKey(); + final SecretKey masterSecret = TestUtils.createSecretKey(); + + KeyAgreementProtocol protocol = + new KeyAgreementProtocol(callbacks, crypto, payloadEncoder, + transport, theirPayload, ourPayload, ourKeyPair, true); + + // expectations + context.checking(new Expectations() {{ + // Helpers + allowing(payloadEncoder).encode(ourPayload); + will(returnValue(ALICE_PAYLOAD)); + allowing(payloadEncoder).encode(theirPayload); + will(returnValue(BOB_PAYLOAD)); + allowing(ourPubKey).getEncoded(); + will(returnValue(ALICE_PUBKEY)); + + // Alice sends her public key + oneOf(transport).sendKey(ALICE_PUBKEY); + + // Alice receives Bob's public key + oneOf(callbacks).connectionWaiting(); + oneOf(transport).receiveKey(); + will(returnValue(BOB_PUBKEY)); + oneOf(callbacks).initialPacketReceived(); + + // Alice verifies Bob's public key + oneOf(crypto).deriveKeyCommitment(BOB_PUBKEY); + will(returnValue(BOB_COMMIT)); + + // Alice computes shared secret + oneOf(crypto).deriveSharedSecret(BOB_PUBKEY, ourKeyPair, true); + will(returnValue(sharedSecret)); + + // Alice sends her confirmation record + oneOf(crypto).deriveConfirmationRecord(sharedSecret, BOB_PAYLOAD, + ALICE_PAYLOAD, BOB_PUBKEY, ourKeyPair, true, true); + will(returnValue(ALICE_CONFIRM)); + oneOf(transport).sendConfirm(ALICE_CONFIRM); + + // Alice receives Bob's confirmation record + oneOf(transport).receiveConfirm(); + will(returnValue(BOB_CONFIRM)); + + // Alice verifies Bob's confirmation record + oneOf(crypto).deriveConfirmationRecord(sharedSecret, BOB_PAYLOAD, + ALICE_PAYLOAD, BOB_PUBKEY, ourKeyPair, true, false); + will(returnValue(BOB_CONFIRM)); + + // Alice computes master secret + oneOf(crypto).deriveMasterSecret(sharedSecret); + will(returnValue(masterSecret)); + }}); + + // execute + assertThat(masterSecret, is(equalTo(protocol.perform()))); + } + + @Test + public void testBobProtocol() throws Exception { + // set up + final Payload theirPayload = new Payload(ALICE_COMMIT, null); + final Payload ourPayload = new Payload(BOB_COMMIT, null); + final KeyPair ourKeyPair = new KeyPair(ourPubKey, null); + final SecretKey sharedSecret = TestUtils.createSecretKey(); + final SecretKey masterSecret = TestUtils.createSecretKey(); + + KeyAgreementProtocol protocol = + new KeyAgreementProtocol(callbacks, crypto, payloadEncoder, + transport, theirPayload, ourPayload, ourKeyPair, false); + + // expectations + context.checking(new Expectations() {{ + // Helpers + allowing(payloadEncoder).encode(ourPayload); + will(returnValue(BOB_PAYLOAD)); + allowing(payloadEncoder).encode(theirPayload); + will(returnValue(ALICE_PAYLOAD)); + allowing(ourPubKey).getEncoded(); + will(returnValue(BOB_PUBKEY)); + + // Bob receives Alice's public key + oneOf(transport).receiveKey(); + will(returnValue(ALICE_PUBKEY)); + oneOf(callbacks).initialPacketReceived(); + + // Bob verifies Alice's public key + oneOf(crypto).deriveKeyCommitment(ALICE_PUBKEY); + will(returnValue(ALICE_COMMIT)); + + // Bob sends his public key + oneOf(transport).sendKey(BOB_PUBKEY); + + // Bob computes shared secret + oneOf(crypto).deriveSharedSecret(ALICE_PUBKEY, ourKeyPair, false); + will(returnValue(sharedSecret)); + + // Bob receives Alices's confirmation record + oneOf(transport).receiveConfirm(); + will(returnValue(ALICE_CONFIRM)); + + // Bob verifies Alice's confirmation record + oneOf(crypto).deriveConfirmationRecord(sharedSecret, ALICE_PAYLOAD, + BOB_PAYLOAD, ALICE_PUBKEY, ourKeyPair, false, true); + will(returnValue(ALICE_CONFIRM)); + + // Bob sends his confirmation record + oneOf(crypto).deriveConfirmationRecord(sharedSecret, ALICE_PAYLOAD, + BOB_PAYLOAD, ALICE_PUBKEY, ourKeyPair, false, false); + will(returnValue(BOB_CONFIRM)); + oneOf(transport).sendConfirm(BOB_CONFIRM); + + // Bob computes master secret + oneOf(crypto).deriveMasterSecret(sharedSecret); + will(returnValue(masterSecret)); + }}); + + // execute + assertThat(masterSecret, is(equalTo(protocol.perform()))); + } + + @Test(expected = AbortException.class) + public void testAliceProtocolAbortOnBadKey() throws Exception { + // set up + final Payload theirPayload = new Payload(BOB_COMMIT, null); + final Payload ourPayload = new Payload(ALICE_COMMIT, null); + final KeyPair ourKeyPair = new KeyPair(ourPubKey, null); + + KeyAgreementProtocol protocol = + new KeyAgreementProtocol(callbacks, crypto, payloadEncoder, + transport, theirPayload, ourPayload, ourKeyPair, true); + + // expectations + context.checking(new Expectations() {{ + // Helpers + allowing(ourPubKey).getEncoded(); + will(returnValue(ALICE_PUBKEY)); + + // Alice sends her public key + oneOf(transport).sendKey(ALICE_PUBKEY); + + // Alice receives a bad public key + oneOf(callbacks).connectionWaiting(); + oneOf(transport).receiveKey(); + will(returnValue(BAD_PUBKEY)); + oneOf(callbacks).initialPacketReceived(); + + // Alice verifies Bob's public key + oneOf(crypto).deriveKeyCommitment(BAD_PUBKEY); + will(returnValue(BAD_COMMIT)); + + // Alice aborts + oneOf(transport).sendAbort(false); + + // Alice never computes shared secret + never(crypto).deriveSharedSecret(BAD_PUBKEY, ourKeyPair, true); + }}); + + // execute + protocol.perform(); + } + + @Test(expected = AbortException.class) + public void testBobProtocolAbortOnBadKey() throws Exception { + // set up + final Payload theirPayload = new Payload(ALICE_COMMIT, null); + final Payload ourPayload = new Payload(BOB_COMMIT, null); + final KeyPair ourKeyPair = new KeyPair(ourPubKey, null); + + KeyAgreementProtocol protocol = + new KeyAgreementProtocol(callbacks, crypto, payloadEncoder, + transport, theirPayload, ourPayload, ourKeyPair, false); + + // expectations + context.checking(new Expectations() {{ + // Helpers + allowing(ourPubKey).getEncoded(); + will(returnValue(BOB_PUBKEY)); + + // Bob receives a bad public key + oneOf(transport).receiveKey(); + will(returnValue(BAD_PUBKEY)); + oneOf(callbacks).initialPacketReceived(); + + // Bob verifies Alice's public key + oneOf(crypto).deriveKeyCommitment(BAD_PUBKEY); + will(returnValue(BAD_COMMIT)); + + // Bob aborts + oneOf(transport).sendAbort(false); + + // Bob never sends his public key + never(transport).sendKey(BOB_PUBKEY); + }}); + + // execute + protocol.perform(); + } + + @Test(expected = AbortException.class) + public void testAliceProtocolAbortOnBadConfirm() throws Exception { + // set up + final Payload theirPayload = new Payload(BOB_COMMIT, null); + final Payload ourPayload = new Payload(ALICE_COMMIT, null); + final KeyPair ourKeyPair = new KeyPair(ourPubKey, null); + final SecretKey sharedSecret = TestUtils.createSecretKey(); + + KeyAgreementProtocol protocol = + new KeyAgreementProtocol(callbacks, crypto, payloadEncoder, + transport, theirPayload, ourPayload, ourKeyPair, true); + + // expectations + context.checking(new Expectations() {{ + // Helpers + allowing(payloadEncoder).encode(ourPayload); + will(returnValue(ALICE_PAYLOAD)); + allowing(payloadEncoder).encode(theirPayload); + will(returnValue(BOB_PAYLOAD)); + allowing(ourPubKey).getEncoded(); + will(returnValue(ALICE_PUBKEY)); + + // Alice sends her public key + oneOf(transport).sendKey(ALICE_PUBKEY); + + // Alice receives Bob's public key + oneOf(callbacks).connectionWaiting(); + oneOf(transport).receiveKey(); + will(returnValue(BOB_PUBKEY)); + oneOf(callbacks).initialPacketReceived(); + + // Alice verifies Bob's public key + oneOf(crypto).deriveKeyCommitment(BOB_PUBKEY); + will(returnValue(BOB_COMMIT)); + + // Alice computes shared secret + oneOf(crypto).deriveSharedSecret(BOB_PUBKEY, ourKeyPair, true); + will(returnValue(sharedSecret)); + + // Alice sends her confirmation record + oneOf(crypto).deriveConfirmationRecord(sharedSecret, BOB_PAYLOAD, + ALICE_PAYLOAD, BOB_PUBKEY, ourKeyPair, true, true); + will(returnValue(ALICE_CONFIRM)); + oneOf(transport).sendConfirm(ALICE_CONFIRM); + + // Alice receives a bad confirmation record + oneOf(transport).receiveConfirm(); + will(returnValue(BAD_CONFIRM)); + + // Alice verifies Bob's confirmation record + oneOf(crypto).deriveConfirmationRecord(sharedSecret, BOB_PAYLOAD, + ALICE_PAYLOAD, BOB_PUBKEY, ourKeyPair, true, false); + will(returnValue(BOB_CONFIRM)); + + // Alice aborts + oneOf(transport).sendAbort(false); + + // Alice never computes master secret + never(crypto).deriveMasterSecret(sharedSecret); + }}); + + // execute + protocol.perform(); + } + + @Test(expected = AbortException.class) + public void testBobProtocolAbortOnBadConfirm() throws Exception { + // set up + final Payload theirPayload = new Payload(ALICE_COMMIT, null); + final Payload ourPayload = new Payload(BOB_COMMIT, null); + final KeyPair ourKeyPair = new KeyPair(ourPubKey, null); + final SecretKey sharedSecret = TestUtils.createSecretKey(); + + KeyAgreementProtocol protocol = + new KeyAgreementProtocol(callbacks, crypto, payloadEncoder, + transport, theirPayload, ourPayload, ourKeyPair, false); + + // expectations + context.checking(new Expectations() {{ + // Helpers + allowing(payloadEncoder).encode(ourPayload); + will(returnValue(BOB_PAYLOAD)); + allowing(payloadEncoder).encode(theirPayload); + will(returnValue(ALICE_PAYLOAD)); + allowing(ourPubKey).getEncoded(); + will(returnValue(BOB_PUBKEY)); + + // Bob receives Alice's public key + oneOf(transport).receiveKey(); + will(returnValue(ALICE_PUBKEY)); + oneOf(callbacks).initialPacketReceived(); + + // Bob verifies Alice's public key + oneOf(crypto).deriveKeyCommitment(ALICE_PUBKEY); + will(returnValue(ALICE_COMMIT)); + + // Bob sends his public key + oneOf(transport).sendKey(BOB_PUBKEY); + + // Bob computes shared secret + oneOf(crypto).deriveSharedSecret(ALICE_PUBKEY, ourKeyPair, false); + will(returnValue(sharedSecret)); + + // Bob receives a bad confirmation record + oneOf(transport).receiveConfirm(); + will(returnValue(BAD_CONFIRM)); + + // Bob verifies Alice's confirmation record + oneOf(crypto).deriveConfirmationRecord(sharedSecret, ALICE_PAYLOAD, + BOB_PAYLOAD, ALICE_PUBKEY, ourKeyPair, false, true); + will(returnValue(ALICE_CONFIRM)); + + // Bob aborts + oneOf(transport).sendAbort(false); + + // Bob never sends his confirmation record + never(crypto).deriveConfirmationRecord(sharedSecret, ALICE_PAYLOAD, + BOB_PAYLOAD, ALICE_PUBKEY, ourKeyPair, false, false); + }}); + + // execute + protocol.perform(); + } +} \ No newline at end of file