diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index cab5c80b9..cb723afa4 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -14,11 +14,13 @@
/>
+
+
@@ -165,6 +167,16 @@
android:value=".android.NavDrawerActivity"
/>
+
+
+
diff --git a/briar-android/build.gradle b/briar-android/build.gradle
index 81228e918..7e79c0dff 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 f7f90efa8..5b167e4ff 100644
--- a/briar-android/res/layout/activity_nav_drawer.xml
+++ b/briar-android/res/layout/activity_nav_drawer.xml
@@ -2,65 +2,12 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 000000000..31f15925b
--- /dev/null
+++ b/briar-android/res/layout/fragment_keyagreement_qr.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index cf63cf76b..f75d8a90d 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -34,6 +34,7 @@
Contacts
No contacts
Add a Contact
+ Add a Contact - Step %1$d/%2$d
Choose the identity you want to use:
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.
Continue
@@ -52,6 +53,14 @@
Codes do not match
This could mean that someone is trying to interfere with your connection
Contact added: %s
+ Contact %s already exists
+ Contact exchange failed
+ Scan QR code
+ The QR code is invalid
+ Connecting to device\u2026
+ Authenticating with device\u2026
+ Connection aborted by us! This could mean that someone is trying to interfere with your connection
+ Connection aborted by your contact! This could mean that someone is trying to interfere with your connection
No messages
Type message
Message sent
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index 4db188d9f..a00d6fb12 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 7b1e6ff53..6bef438cf 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 30d7ecb5d..0d7c5d323 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 000000000..4615cd728
--- /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 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 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 000000000..185d3c0b1
--- /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 000000000..df18a12fc
--- /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 openTask =
+ new AsyncTask() {
+ @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 000000000..623650e66
--- /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 sceneModes = params.getSupportedSceneModes();
+ if (sceneModes == null) sceneModes = Collections.emptyList();
+ List 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 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 000000000..e2fb6eed9
--- /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 000000000..5fdc54c39
--- /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 {
+
+ 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 000000000..ef8ff6ec6
--- /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 2bc7dbe70..40a8a0410 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 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 listen() {
+ return new Callable() {
+ @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 e5f7180c0..d3dbff793 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 000000000..50bf543c7
--- /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 000000000..0d85850f6
--- /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 0ba779202..11631f22f 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.
*
- * 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 000000000..5e217f7f4
--- /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 000000000..97313a276
--- /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 000000000..f2afe349e
--- /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 000000000..df3b2cbac
--- /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 000000000..a12a9c459
--- /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 000000000..473c6538a
--- /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 000000000..7dfa3bc68
--- /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 521789244..f23911126 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 000000000..05163614c
--- /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 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 000000000..3f772ca28
--- /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 000000000..443ef42bd
--- /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.
+ *
+ * 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 000000000..f823fcacd
--- /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 000000000..434f9cb71
--- /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 000000000..0c749da53
--- /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 {
+
+ private final Bytes commitment;
+ private final List descriptors;
+
+ public Payload(byte[] commitment, List 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 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 000000000..31876c102
--- /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 000000000..0df9c653d
--- /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 000000000..ef1d2ff51
--- /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 000000000..cdaa5a579
--- /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 35248962d..838c66ff8 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 getInvitationPlugins();
+
+ /** Returns any running duplex plugins that support key agreement. */
+ Collection 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 519400b56..ff7db3b51 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 7fe4201a7..94452e1e0 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 11d6c87b0..6303f210b 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 000000000..943d02de9
--- /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 bd8e2d3af..fb5c290ce 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 2a779f3fe..825c3720f 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 210d173d9..b2947ece4 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 f2c6e2619..1460b953e 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 000000000..670bbc3ee
--- /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 000000000..e87297af6
--- /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 connect;
+
+ private final List listeners =
+ new ArrayList();
+ private final List> pending =
+ new ArrayList>();
+
+ 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(
+ 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 descriptors =
+ new ArrayList();
+ 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 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 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 {
+
+ 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 {
+
+ private final Callable connectionTask;
+
+ private ReadableTask(Callable 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 000000000..9f6a52902
--- /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 000000000..c832edd09
--- /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.
+ *
+ * Alice:
+ *
+ * - Send A_KEY
+ * - Receive B_KEY
+ *
+ * - Check B_KEY matches B_COMMIT
+ *
+ * - Calculate s
+ * - Send A_CONFIRM
+ * - Receive B_CONFIRM
+ *
+ * - Check B_CONFIRM matches expected
+ *
+ * - Derive master
+ *
+ * Bob:
+ *
+ * - Receive A_KEY
+ *
+ * - Check A_KEY matches A_COMMIT
+ *
+ * - Send B_KEY
+ * - Calculate s
+ * - Receive A_CONFIRM
+ *
+ * - Check A_CONFIRM matches expected
+ *
+ * - Send B_CONFIRM
+ * - Derive master
+ *
+ */
+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 000000000..aafde49b3
--- /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 000000000..12f767874
--- /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 000000000..25a18962b
--- /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 000000000..8a26f9405
--- /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 000000000..d13f9ff73
--- /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 descriptors = new ArrayList();
+ 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 367b1b66c..ef6e65e19 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 getKeyAgreementPlugins() {
+ List supported = new ArrayList();
+ 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 54376310a..d6effe2a7 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 getLocalIpAddresses() {
List 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 79e289c39..875112c5e 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 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 listen() {
+ return new Callable() {
+ @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 ffa06c2a4..bbf71da9c 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 c5ae11237..b3c2af66e 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 000000000..e404c547f
--- /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