Merge branch '117-qr-contacts' into 'master'

BQP with QR codes

This MR implements BQP for key agreement over short-range transports. It also implements the Android UI for using BQP with QR codes.

Closes #117.

See merge request !84
This commit is contained in:
akwizgran
2016-03-31 11:21:02 +00:00
63 changed files with 3571 additions and 125 deletions

View File

@@ -14,11 +14,13 @@
/>
<uses-feature android:name="android.hardware.bluetooth"/>
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.VIBRATE" />
@@ -165,6 +167,16 @@
android:value=".android.NavDrawerActivity"
/>
</activity>
<activity
android:name=".android.keyagreement.KeyAgreementActivity"
android:label="@string/add_contact_title"
android:theme="@style/BriarThemeNoActionBar.Default"
android:parentActivityName=".android.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.NavDrawerActivity"
/>
</activity>
<activity
android:name=".android.StartupFailureActivity"
android:label="@string/startup_failed_activity_title">

View File

@@ -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
}
}
}

View File

@@ -2,65 +2,12 @@
<android.support.v4.widget.DrawerLayout
android:id="@+id/drawer_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The first child(root) is the content view -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/BriarToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/content_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/default_background"/>
<RelativeLayout
android:id="@+id/container_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/default_background"
android:visibility="invisible"
tools:visibility="visible">
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleLargeInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/title_progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/progress_bar"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="progress bar title"
/>
</RelativeLayout>
</FrameLayout>
</LinearLayout>
<include
layout="@layout/activity_with_loading"/>
<!-- The second child is the menu -->
<include

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/BriarToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/content_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/default_background"/>
<RelativeLayout
android:id="@+id/container_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/default_background"
android:visibility="invisible"
tools:visibility="visible">
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleLargeInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/title_progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/progress_bar"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="progress bar title"
/>
</RelativeLayout>
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/qr_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="2">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/black"
android:gravity="center">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_light">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium">
<ProgressBar
style="?android:attr/progressBarStyleInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/margin_large"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
</RelativeLayout>
<org.briarproject.android.util.CameraView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitCenter"/>
</LinearLayout>

View File

@@ -34,6 +34,7 @@
<string name="contact_list_title">Contacts</string>
<string name="no_contacts">No contacts</string>
<string name="add_contact_title">Add a Contact</string>
<string name="add_contact_title_step">Add a Contact - Step %1$d/%2$d</string>
<string name="your_nickname">Choose the identity you want to use:</string>
<string name="face_to_face">You must be face-to-face with the person you want to add as a contact. This will prevent anyone from impersonating you or reading your messages in future.</string>
<string name="continue_button">Continue</string>
@@ -52,6 +53,14 @@
<string name="codes_do_not_match">Codes do not match</string>
<string name="interfering">This could mean that someone is trying to interfere with your connection</string>
<string name="contact_added_toast">Contact added: %s</string>
<string name="contact_already_exists">Contact %s already exists</string>
<string name="contact_exchange_failed">Contact exchange failed</string>
<string name="scan_qr_code">Scan QR code</string>
<string name="qr_code_invalid">The QR code is invalid</string>
<string name="connecting_to_device">Connecting to device\u2026</string>
<string name="authenticating_with_device">Authenticating with device\u2026</string>
<string name="connection_aborted_local">Connection aborted by us! This could mean that someone is trying to interfere with your connection</string>
<string name="connection_aborted_remote">Connection aborted by your contact! This could mean that someone is trying to interfere with your connection</string>
<string name="no_private_messages">No messages</string>
<string name="private_message_hint">Type message</string>
<string name="message_sent_toast">Message sent</string>

View File

@@ -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);

View File

@@ -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) {
/*

View File

@@ -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));
}
});

View File

@@ -0,0 +1,193 @@
package org.briarproject.android.keyagreement;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Spinner;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.identity.LocalAuthorItem;
import org.briarproject.android.identity.LocalAuthorItemComparator;
import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.app.Activity.RESULT_OK;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.identity.LocalAuthorItem.NEW;
public class ChooseIdentityFragment extends BaseFragment
implements OnItemSelectedListener {
interface IdentitySelectedListener {
void identitySelected(AuthorId localAuthorId);
}
private static final Logger LOG =
Logger.getLogger(ChooseIdentityFragment.class.getName());
public static final String TAG = "ChooseIdentityFragment";
private static final int REQUEST_CREATE_IDENTITY = 1;
private IdentitySelectedListener lsnr;
private LocalAuthorSpinnerAdapter adapter;
private Spinner spinner;
private View button;
private AuthorId localAuthorId;
// Fields that are accessed from background threads must be volatile
@Inject
protected volatile IdentityManager identityManager;
public static ChooseIdentityFragment newInstance() {
Bundle args = new Bundle();
ChooseIdentityFragment fragment = new ChooseIdentityFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
lsnr = (IdentitySelectedListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(
"Using class must implement IdentitySelectedListener");
}
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.invitation_bluetooth_start, container,
false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
adapter = new LocalAuthorSpinnerAdapter(getActivity(), false);
spinner = (Spinner) view.findViewById(R.id.spinner);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(this);
button = view.findViewById(R.id.continueButton);
button.setEnabled(false);
button.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View view) {
lsnr.identitySelected(localAuthorId);
}
});
loadLocalAuthors();
}
private void loadLocalAuthors() {
listener.runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
Collection<LocalAuthor> authors =
identityManager.getLocalAuthors();
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading authors took " + duration + " ms");
displayLocalAuthors(authors);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayLocalAuthors(final Collection<LocalAuthor> authors) {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.clear();
for (LocalAuthor a : authors)
adapter.add(new LocalAuthorItem(a));
adapter.sort(LocalAuthorItemComparator.INSTANCE);
// If a local author has been selected, select it again
if (localAuthorId == null) return;
int count = adapter.getCount();
for (int i = 0; i < count; i++) {
LocalAuthorItem item = adapter.getItem(i);
if (item == NEW) continue;
if (item.getLocalAuthor().getId().equals(localAuthorId)) {
spinner.setSelection(i);
return;
}
}
}
});
}
private void setLocalAuthorId(AuthorId authorId) {
localAuthorId = authorId;
button.setEnabled(localAuthorId != null);
}
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
LocalAuthorItem item = adapter.getItem(position);
if (item == NEW) {
setLocalAuthorId(null);
Intent i = new Intent(getActivity(), CreateIdentityActivity.class);
startActivityForResult(i, REQUEST_CREATE_IDENTITY);
} else {
setLocalAuthorId(item.getLocalAuthor().getId());
}
}
public void onNothingSelected(AdapterView<?> parent) {
setLocalAuthorId(null);
}
@Override
public void onActivityResult(int request, int result, Intent data) {
if (request == REQUEST_CREATE_IDENTITY && result == RESULT_OK) {
byte[] b = data.getByteArrayExtra("briar.LOCAL_AUTHOR_ID");
if (b == null) throw new IllegalStateException();
setLocalAuthorId(new AuthorId(b));
loadLocalAuthors();
} else
super.onActivityResult(request, result, data);
}
}

View File

@@ -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();
}
});
}
}

View File

@@ -0,0 +1,371 @@
package org.briarproject.android.keyagreement;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.Camera;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Base64;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.Result;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.fragment.BaseEventFragment;
import org.briarproject.android.util.AndroidUtils;
import org.briarproject.android.util.CameraView;
import org.briarproject.android.util.QrCodeDecoder;
import org.briarproject.android.util.QrCodeUtils;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.KeyAgreementAbortedEvent;
import org.briarproject.api.event.KeyAgreementFailedEvent;
import org.briarproject.api.event.KeyAgreementListeningEvent;
import org.briarproject.api.event.KeyAgreementStartedEvent;
import org.briarproject.api.event.KeyAgreementWaitingEvent;
import org.briarproject.api.keyagreement.KeyAgreementTask;
import org.briarproject.api.keyagreement.KeyAgreementTaskFactory;
import org.briarproject.api.keyagreement.Payload;
import org.briarproject.api.keyagreement.PayloadEncoder;
import org.briarproject.api.keyagreement.PayloadParser;
import java.io.IOException;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
@SuppressWarnings("deprecation")
public class ShowQrCodeFragment extends BaseEventFragment
implements QrCodeDecoder.ResultCallback {
private static final Logger LOG =
Logger.getLogger(ShowQrCodeFragment.class.getName());
public static final String TAG = "ShowQrCodeFragment";
@Inject
protected KeyAgreementTaskFactory keyAgreementTaskFactory;
@Inject
protected PayloadEncoder payloadEncoder;
@Inject
protected PayloadParser payloadParser;
private LinearLayout qrLayout;
private CameraView cameraView;
private TextView status;
private ImageView qrCode;
private BluetoothStateReceiver receiver;
private QrCodeDecoder decoder;
private boolean gotRemotePayload;
private volatile KeyAgreementTask task;
private volatile BluetoothAdapter adapter;
private volatile boolean waitingForBluetooth;
public static ShowQrCodeFragment newInstance() {
Bundle args = new Bundle();
ShowQrCodeFragment fragment = new ShowQrCodeFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_keyagreement_qr, container,
false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
qrLayout = (LinearLayout) view.findViewById(R.id.qr_layout);
cameraView = (CameraView) view.findViewById(R.id.camera_view);
status = (TextView) view.findViewById(R.id.connect_status);
qrCode = (ImageView) view.findViewById(R.id.qr_code);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
decoder = new QrCodeDecoder(this);
Display display = getActivity().getWindowManager().getDefaultDisplay();
boolean portrait = display.getWidth() < display.getHeight();
qrLayout.setOrientation(portrait ? VERTICAL : HORIZONTAL);
adapter = BluetoothAdapter.getDefaultAdapter();
}
@Override
public void onStart() {
super.onStart();
// Listen for changes to the Bluetooth state
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
receiver = new BluetoothStateReceiver();
getActivity().registerReceiver(receiver, filter);
// Enable BT adapter if it is not already on.
if (adapter != null && !adapter.isEnabled()) {
waitingForBluetooth = true;
AndroidUtils.enableBluetooth(adapter, true);
} else
startListening();
}
@Override
public void onResume() {
super.onResume();
if (!gotRemotePayload) openCamera();
}
@Override
public void onPause() {
super.onPause();
if (!gotRemotePayload) releaseCamera();
}
@Override
public void onStop() {
super.onStop();
stopListening();
if (receiver != null) getActivity().unregisterReceiver(receiver);
}
private void startListening() {
task = keyAgreementTaskFactory.getTask();
gotRemotePayload = false;
new Thread(new Runnable() {
@Override
public void run() {
task.listen();
}
}).start();
}
private void stopListening() {
new Thread(new Runnable() {
@Override
public void run() {
task.stopListening();
}
}).start();
}
private void openCamera() {
AsyncTask<Void, Void, Camera> openTask =
new AsyncTask<Void, Void, Camera>() {
@Override
protected Camera doInBackground(Void... unused) {
LOG.info("Opening camera");
try {
return Camera.open();
} catch (RuntimeException e) {
LOG.log(WARNING,
"Error opening camera, trying again", e);
try {
Thread.sleep(1000);
} catch (InterruptedException e2) {
LOG.info("Interrupted before second attempt");
return null;
}
try {
return Camera.open();
} catch (RuntimeException e2) {
LOG.log(WARNING, "Error opening camera", e2);
return null;
}
}
}
@Override
protected void onPostExecute(Camera camera) {
if (camera == null) {
// TODO better solution?
getActivity().finish();
} else {
cameraView.start(camera, decoder, 0);
}
}
};
openTask.execute();
}
private void releaseCamera() {
LOG.info("Releasing camera");
try {
cameraView.stop();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error releasing camera", e);
// TODO better solution
getActivity().finish();
}
}
private void reset() {
cameraView.setVisibility(View.VISIBLE);
startListening();
openCamera();
}
private void qrCodeScanned(String content) {
try {
// TODO use Base32
Payload remotePayload = payloadParser.parse(
Base64.decode(content, 0));
cameraView.setVisibility(View.GONE);
status.setText(R.string.connecting_to_device);
task.connectAndRunProtocol(remotePayload);
} catch (IOException e) {
// TODO show failure
Toast.makeText(getActivity(), R.string.qr_code_invalid,
LENGTH_LONG).show();
}
}
@Override
public void eventOccurred(Event e) {
if (e instanceof KeyAgreementListeningEvent) {
KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e;
setQrCode(event.getLocalPayload());
} else if (e instanceof KeyAgreementFailedEvent) {
keyAgreementFailed();
} else if (e instanceof KeyAgreementWaitingEvent) {
keyAgreementWaiting();
} else if (e instanceof KeyAgreementStartedEvent) {
keyAgreementStarted();
} else if (e instanceof KeyAgreementAbortedEvent) {
KeyAgreementAbortedEvent event = (KeyAgreementAbortedEvent) e;
keyAgreementAborted(event.didRemoteAbort());
}
}
private void setQrCode(final Payload localPayload) {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
// TODO use Base32
String input = Base64.encodeToString(
payloadEncoder.encode(localPayload), 0);
qrCode.setImageBitmap(
QrCodeUtils.createQrCode(getActivity(), input));
// Simple fade-in animation
AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
anim.setDuration(200);
qrCode.startAnimation(anim);
}
});
}
private void keyAgreementFailed() {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
reset();
// TODO show failure somewhere persistent?
Toast.makeText(getActivity(), R.string.connection_failed,
LENGTH_LONG).show();
}
});
}
private void keyAgreementWaiting() {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
status.setText(R.string.waiting_for_contact);
}
});
}
private void keyAgreementStarted() {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.showLoadingScreen(false,
R.string.authenticating_with_device);
}
});
}
private void keyAgreementAborted(final boolean remoteAborted) {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
reset();
listener.hideLoadingScreen();
// TODO show abort somewhere persistent?
Toast.makeText(getActivity(),
remoteAborted ? R.string.connection_aborted_remote :
R.string.connection_aborted_local, LENGTH_LONG)
.show();
}
});
}
@Override
public void handleResult(final Result result) {
listener.runOnUiThread(new Runnable() {
public void run() {
LOG.info("Got result from decoder");
if (!gotRemotePayload) {
gotRemotePayload = true;
releaseCamera();
qrCodeScanned(result.getText());
}
}
});
}
private class BluetoothStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
if (state == STATE_ON && waitingForBluetooth) {
LOG.info("Bluetooth enabled");
waitingForBluetooth = false;
startListening();
}
}
}
}

View File

@@ -0,0 +1,268 @@
package org.briarproject.android.util;
import android.content.Context;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.os.Build;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO;
import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE;
import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO;
import static android.hardware.Camera.Parameters.FOCUS_MODE_EDOF;
import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED;
import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO;
import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE;
import static android.view.SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@SuppressWarnings("deprecation")
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
AutoFocusCallback {
private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds
private static final Logger LOG =
Logger.getLogger(CameraView.class.getName());
private Camera camera = null;
private PreviewConsumer previewConsumer = null;
private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0;
private boolean autoFocus = false, surfaceExists = false;
public CameraView(Context context) {
super(context);
initialize();
}
public CameraView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
private void initialize() {
setKeepScreenOn(true);
SurfaceHolder holder = getHolder();
if (Build.VERSION.SDK_INT < 11)
holder.setType(SURFACE_TYPE_PUSH_BUFFERS);
holder.addCallback(this);
}
public void start(Camera camera, PreviewConsumer previewConsumer,
int rotationDegrees) {
this.camera = camera;
this.previewConsumer = previewConsumer;
setDisplayOrientation(rotationDegrees);
Parameters params = camera.getParameters();
setFocusMode(params);
setPreviewSize(params);
applyParameters(params);
if (surfaceExists) startPreview(getHolder());
}
public void stop() {
stopPreview();
try {
camera.release();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error releasing camera", e);
}
camera = null;
}
private void startPreview(SurfaceHolder holder) {
try {
camera.setPreviewDisplay(holder);
camera.startPreview();
if (autoFocus) camera.autoFocus(this);
previewConsumer.start(camera);
} catch (IOException e) {
LOG.log(WARNING, "Error starting camera preview", e);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error starting camera preview", e);
}
}
private void stopPreview() {
try {
previewConsumer.stop();
if (autoFocus) camera.cancelAutoFocus();
camera.stopPreview();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error stopping camera preview", e);
}
}
private void setDisplayOrientation(int rotationDegrees) {
int orientation;
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(0, info);
if (info.facing == CAMERA_FACING_FRONT) {
orientation = (info.orientation + rotationDegrees) % 360;
orientation = (360 - orientation) % 360;
} else {
orientation = (info.orientation - rotationDegrees + 360) % 360;
}
if(LOG.isLoggable(INFO))
LOG.info("Display orientation " + orientation + " degrees");
try {
camera.setDisplayOrientation(orientation);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error setting display orientation", e);
}
displayOrientation = orientation;
}
private void setFocusMode(Parameters params) {
if (Build.VERSION.SDK_INT >= 15 &&
params.isVideoStabilizationSupported()) {
LOG.info("Enabling video stabilisation");
params.setVideoStabilization(true);
}
// This returns null on the HTC Wildfire S
List<String> sceneModes = params.getSupportedSceneModes();
if (sceneModes == null) sceneModes = Collections.emptyList();
List<String> focusModes = params.getSupportedFocusModes();
if (LOG.isLoggable(INFO)) {
LOG.info("Scene modes: " + sceneModes);
LOG.info("Focus modes: " + focusModes);
}
if (sceneModes.contains(SCENE_MODE_BARCODE)) {
LOG.info("Setting scene mode to barcode");
params.setSceneMode(SCENE_MODE_BARCODE);
} else if (Build.VERSION.SDK_INT >= 14 &&
focusModes.contains(FOCUS_MODE_CONTINUOUS_PICTURE)) {
LOG.info("Setting focus mode to continuous picture");
params.setFocusMode(FOCUS_MODE_CONTINUOUS_PICTURE);
} else if (focusModes.contains(FOCUS_MODE_CONTINUOUS_VIDEO)) {
LOG.info("Setting focus mode to continuous video");
params.setFocusMode(FOCUS_MODE_CONTINUOUS_VIDEO);
} else if (focusModes.contains(FOCUS_MODE_EDOF)) {
LOG.info("Setting focus mode to EDOF");
params.setFocusMode(FOCUS_MODE_EDOF);
} else if (focusModes.contains(FOCUS_MODE_MACRO)) {
LOG.info("Setting focus mode to macro");
params.setFocusMode(FOCUS_MODE_MACRO);
autoFocus = true;
} else if (focusModes.contains(FOCUS_MODE_AUTO)) {
LOG.info("Setting focus mode to auto");
params.setFocusMode(FOCUS_MODE_AUTO);
autoFocus = true;
} else if (focusModes.contains(FOCUS_MODE_FIXED)) {
LOG.info("Setting focus mode to fixed");
params.setFocusMode(FOCUS_MODE_FIXED);
} else {
LOG.info("No suitable focus mode");
}
params.setZoom(0);
}
private void setPreviewSize(Parameters params) {
if (surfaceWidth == 0 || surfaceHeight == 0) return;
float idealRatio = (float) surfaceWidth / surfaceHeight;
DisplayMetrics screen = getContext().getResources().getDisplayMetrics();
int screenMax = Math.max(screen.widthPixels, screen.heightPixels);
boolean rotatePreview = displayOrientation % 180 == 90;
List<Size> sizes = params.getSupportedPreviewSizes();
Size bestSize = null;
float bestScore = 0;
for (Size size : sizes) {
int width = rotatePreview ? size.height : size.width;
int height = rotatePreview ? size.width : size.height;
float ratio = (float) width / height;
float stretch = Math.max(ratio / idealRatio, idealRatio / ratio);
int pixels = width * height;
float score = width * height / stretch;
if (LOG.isLoggable(INFO)) {
LOG.info("Size " + size.width + "x" + size.height
+ ", stretch " + stretch + ", pixels " + pixels
+ ", score " + score);
}
// Large preview sizes can crash older devices
int maxDimension = Math.max(width, height);
if (Build.VERSION.SDK_INT < 14 && maxDimension > screenMax) {
LOG.info("Too large for screen");
continue;
}
if (bestSize == null || score > bestScore) {
bestSize = size;
bestScore = score;
}
}
if (bestSize != null) {
if(LOG.isLoggable(INFO))
LOG.info("Best size " + bestSize.width + "x" + bestSize.height);
params.setPreviewSize(bestSize.width, bestSize.height);
}
}
private void applyParameters(Parameters params) {
try {
camera.setParameters(params);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error setting camera parameters", e);
}
}
public void surfaceCreated(SurfaceHolder holder) {
LOG.info("Surface created");
surfaceExists = true;
if (camera != null) startPreview(holder);
}
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
if (LOG.isLoggable(INFO)) LOG.info("Surface changed: " + w + "x" + h);
surfaceWidth = w;
surfaceHeight = h;
if (camera == null) return; // We are stopped
stopPreview();
try {
Parameters params = camera.getParameters();
setPreviewSize(params);
applyParameters(params);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error getting camera parameters", e);
}
startPreview(holder);
}
public void surfaceDestroyed(SurfaceHolder holder) {
LOG.info("Surface destroyed");
surfaceExists = false;
holder.removeCallback(this);
}
public void onAutoFocus(boolean success, final Camera camera) {
LOG.info("Auto focus succeeded: " + success);
postDelayed(new Runnable() {
public void run() {
retryAutoFocus();
}
}, AUTO_FOCUS_RETRY_DELAY);
}
private void retryAutoFocus() {
try {
if (camera != null) camera.autoFocus(this);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error retrying auto focus", e);
}
}
}

View File

@@ -0,0 +1,11 @@
package org.briarproject.android.util;
import android.hardware.Camera;
@SuppressWarnings("deprecation")
public interface PreviewConsumer {
void start(Camera camera);
void stop();
}

View File

@@ -0,0 +1,100 @@
package org.briarproject.android.util;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.os.AsyncTask;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.LuminanceSource;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import java.util.logging.Logger;
import static java.util.logging.Level.INFO;
@SuppressWarnings("deprecation")
public class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
private static final Logger LOG =
Logger.getLogger(QrCodeDecoder.class.getName());
private final Reader reader = new QRCodeReader();
private final ResultCallback callback;
private boolean stopped = false;
public QrCodeDecoder(ResultCallback callback) {
this.callback = callback;
}
public void start(Camera camera) {
stopped = false;
askForPreviewFrame(camera);
}
public void stop() {
stopped = true;
}
private void askForPreviewFrame(Camera camera) {
if (!stopped) camera.setOneShotPreviewCallback(this);
}
public void onPreviewFrame(byte[] data, Camera camera) {
if (!stopped) {
Size size = camera.getParameters().getPreviewSize();
new DecoderTask(camera, data, size.width, size.height).execute();
}
}
private class DecoderTask extends AsyncTask<Void, Void, Void> {
final Camera camera;
final byte[] data;
final int width, height;
DecoderTask(Camera camera, byte[] data, int width, int height) {
this.camera = camera;
this.data = data;
this.width = width;
this.height = height;
}
@Override
protected Void doInBackground(Void... params) {
long now = System.currentTimeMillis();
LuminanceSource src = new PlanarYUVLuminanceSource(data, width,
height, 0, 0, width, height, false);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src));
Result result = null;
try {
result = reader.decode(bitmap);
} catch (ReaderException e) {
return null; // No barcode found
} finally {
reader.reset();
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Decoding barcode took " + duration + " ms");
callback.handleResult(result);
return null;
}
@Override
protected void onPostExecute(Void result) {
askForPreviewFrame(camera);
}
}
public interface ResultCallback {
void handleResult(Result result);
}
}

View File

@@ -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;
}
}
}

View File

@@ -14,6 +14,9 @@ import org.briarproject.api.TransportId;
import org.briarproject.android.api.AndroidExecutor;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.keyagreement.KeyAgreementConnection;
import org.briarproject.api.keyagreement.KeyAgreementListener;
import org.briarproject.api.keyagreement.TransportDescriptor;
import org.briarproject.api.plugins.Backoff;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
@@ -67,6 +70,9 @@ class DroidtoothPlugin implements DuplexPlugin {
private static final String DISCOVERY_FINISHED =
"android.bluetooth.adapter.action.DISCOVERY_FINISHED";
private static final String PROP_ADDRESS = "address";
private static final String PROP_UUID = "uuid";
private final Executor ioExecutor;
private final AndroidExecutor androidExecutor;
private final Context appContext;
@@ -161,7 +167,7 @@ class DroidtoothPlugin implements DuplexPlugin {
if (!StringUtils.isNullOrEmpty(address)) {
// Advertise the Bluetooth address to contacts
TransportProperties p = new TransportProperties();
p.put("address", address);
p.put(PROP_ADDRESS, address);
callback.mergeLocalProperties(p);
}
// Bind a server socket to accept connections from contacts
@@ -187,13 +193,13 @@ class DroidtoothPlugin implements DuplexPlugin {
}
private UUID getUuid() {
String uuid = callback.getLocalProperties().get("uuid");
String uuid = callback.getLocalProperties().get(PROP_UUID);
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
TransportProperties p = new TransportProperties();
p.put("uuid", uuid);
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
}
return UUID.fromString(uuid);
@@ -264,9 +270,9 @@ class DroidtoothPlugin implements DuplexPlugin {
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
final ContactId c = e.getKey();
if (connected.contains(c)) continue;
final String address = e.getValue().get("address");
final String address = e.getValue().get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) continue;
final String uuid = e.getValue().get("uuid");
final String uuid = e.getValue().get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) continue;
ioExecutor.execute(new Runnable() {
public void run() {
@@ -325,9 +331,9 @@ class DroidtoothPlugin implements DuplexPlugin {
if (!isRunning()) return null;
TransportProperties p = callback.getRemoteProperties().get(c);
if (p == null) return null;
String address = p.get("address");
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get("uuid");
String uuid = p.get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) return null;
BluetoothSocket s = connect(address, uuid);
if (s == null) return null;
@@ -417,6 +423,48 @@ class DroidtoothPlugin implements DuplexPlugin {
});
}
public boolean supportsKeyAgreement() {
return true;
}
public KeyAgreementListener createKeyAgreementListener(
byte[] localCommitment) {
// No truncation necessary because COMMIT_LENGTH = 16
UUID uuid = UUID.nameUUIDFromBytes(localCommitment);
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
// Bind a server socket for receiving invitation connections
BluetoothServerSocket ss;
try {
ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
}
TransportProperties p = new TransportProperties();
String address = AndroidUtils.getBluetoothAddress(appContext, adapter);
if (!StringUtils.isNullOrEmpty(address))
p.put(PROP_ADDRESS, address);
TransportDescriptor d = new TransportDescriptor(ID, p);
return new BluetoothKeyAgreementListener(d, ss);
}
public DuplexTransportConnection createKeyAgreementConnection(
byte[] remoteCommitment, TransportDescriptor d, long timeout) {
if (!isRunning()) return null;
if (!ID.equals(d.getIdentifier())) return null;
TransportProperties p = d.getProperties();
if (p == null) return null;
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
// No truncation necessary because COMMIT_LENGTH = 16
UUID uuid = UUID.nameUUIDFromBytes(remoteCommitment);
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
BluetoothSocket s = connect(address, uuid.toString());
if (s == null) return null;
return new DroidtoothTransportConnection(this, s);
}
private class BluetoothStateReceiver extends BroadcastReceiver {
@Override
@@ -545,4 +593,39 @@ class DroidtoothPlugin implements DuplexPlugin {
return s;
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final BluetoothServerSocket ss;
public BluetoothKeyAgreementListener(TransportDescriptor descriptor,
BluetoothServerSocket ss) {
super(descriptor);
this.ss = ss;
}
@Override
public Callable<KeyAgreementConnection> listen() {
return new Callable<KeyAgreementConnection>() {
@Override
public KeyAgreementConnection call() throws IOException {
BluetoothSocket s = ss.accept();
if (LOG.isLoggable(INFO))
LOG.info(ID.getString() + ": Incoming connection");
return new KeyAgreementConnection(
new DroidtoothTransportConnection(
DroidtoothPlugin.this, s), ID);
}
};
}
@Override
public void close() {
try {
ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
}
}

View File

@@ -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");

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -36,18 +36,17 @@ public interface CryptoComponent {
int deriveBTConfirmationCode(SecretKey master, boolean alice);
/**
* Derives a header key for an invitation stream from the given master
* secret.
* Derives a stream header key from the given master secret.
* @param alice whether the key is for use by Alice or Bob.
*/
SecretKey deriveBTInvitationKey(SecretKey master, boolean alice);
SecretKey deriveHeaderKey(SecretKey master, boolean alice);
/**
* Derives a nonce from the given master secret for one of the parties to
* sign.
* @param alice whether the nonce is for use by Alice or Bob.
*/
byte[] deriveBTSignatureNonce(SecretKey master, boolean alice);
byte[] deriveSignatureNonce(SecretKey master, boolean alice);
/**
* Derives a commitment to the provided public key.
@@ -107,7 +106,7 @@ public interface CryptoComponent {
* Derives a master secret from two public keys and one of the corresponding
* private keys.
* <p/>
* Part of BQP. This is a helper method that calls
* This is a helper method that calls
* deriveMasterSecret(deriveSharedSecret(theirPublicKey, ourKeyPair, alice))
*
* @param theirPublicKey the ephemeral public key of the remote party

View File

@@ -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;
}
}

View File

@@ -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 {
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
package org.briarproject.api.event;
/** An event that is broadcast when a BQP protocol completes. */
public class KeyAgreementStartedEvent extends Event {
}

View File

@@ -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 {
}

View File

@@ -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;
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,35 @@
package org.briarproject.api.keyagreement;
import java.util.concurrent.Callable;
/**
* An class for managing a particular key agreement listener.
*/
public abstract class KeyAgreementListener {
private final TransportDescriptor descriptor;
public KeyAgreementListener(TransportDescriptor descriptor) {
this.descriptor = descriptor;
}
/**
* Returns the descriptor that a remote peer can use to connect to this
* listener.
*/
public TransportDescriptor getDescriptor() {
return descriptor;
}
/**
* Starts listening for incoming connections, and returns a Callable that
* will return a KeyAgreementConnection when an incoming connection is
* received.
*/
public abstract Callable<KeyAgreementConnection> listen();
/**
* Closes the underlying server socket.
*/
public abstract void close();
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.api.keyagreement;
/** A task for conducting a key agreement with a remote peer. */
public interface KeyAgreementTask {
/**
* Start listening for short-range BQP connections, if we are not already.
* <p/>
* Will trigger a KeyAgreementListeningEvent containing the local Payload,
* even if we are already listening.
*/
void listen();
/**
* Stop listening for short-range BQP connections.
*/
void stopListening();
/** Asynchronously start the connection process. */
void connectAndRunProtocol(Payload remotePayload);
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,34 @@
package org.briarproject.api.keyagreement;
import org.briarproject.api.Bytes;
import java.util.List;
/**
* A BQP payload.
*/
public class Payload implements Comparable<Payload> {
private final Bytes commitment;
private final List<TransportDescriptor> descriptors;
public Payload(byte[] commitment, List<TransportDescriptor> descriptors) {
this.commitment = new Bytes(commitment);
this.descriptors = descriptors;
}
/** Returns the commitment contained in this payload. */
public byte[] getCommitment() {
return commitment.getBytes();
}
/** Returns the transport descriptors contained in this payload. */
public List<TransportDescriptor> getTransportDescriptors() {
return descriptors;
}
@Override
public int compareTo(Payload p) {
return commitment.compareTo(p.commitment);
}
}

View File

@@ -0,0 +1,6 @@
package org.briarproject.api.keyagreement;
public interface PayloadEncoder {
byte[] encode(Payload p);
}

View File

@@ -0,0 +1,8 @@
package org.briarproject.api.keyagreement;
import java.io.IOException;
public interface PayloadParser {
Payload parse(byte[] raw) throws IOException;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -19,4 +19,7 @@ public interface PluginManager {
/** Returns any running duplex plugins that support invitations. */
Collection<DuplexPlugin> getInvitationPlugins();
/** Returns any running duplex plugins that support key agreement. */
Collection<DuplexPlugin> getKeyAgreementPlugins();
}

View File

@@ -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);
}

View File

@@ -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',
]
}

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -0,0 +1,236 @@
package org.briarproject.keyagreement;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyPair;
import org.briarproject.api.keyagreement.KeyAgreementConnection;
import org.briarproject.api.keyagreement.KeyAgreementListener;
import org.briarproject.api.keyagreement.Payload;
import org.briarproject.api.keyagreement.TransportDescriptor;
import org.briarproject.api.plugins.PluginManager;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.system.Clock;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.logging.Logger;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.api.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT;
class KeyAgreementConnector {
interface Callbacks {
void connectionWaiting();
}
private static final Logger LOG =
Logger.getLogger(KeyAgreementConnector.class.getName());
private final Callbacks callbacks;
private final Clock clock;
private final CryptoComponent crypto;
private final PluginManager pluginManager;
private final CompletionService<KeyAgreementConnection> connect;
private final List<KeyAgreementListener> listeners =
new ArrayList<KeyAgreementListener>();
private final List<Future<KeyAgreementConnection>> pending =
new ArrayList<Future<KeyAgreementConnection>>();
private volatile boolean connecting = false;
private volatile boolean alice = false;
public KeyAgreementConnector(Callbacks callbacks, Clock clock,
CryptoComponent crypto, PluginManager pluginManager,
Executor ioExecutor) {
this.callbacks = callbacks;
this.clock = clock;
this.crypto = crypto;
this.pluginManager = pluginManager;
connect = new ExecutorCompletionService<KeyAgreementConnection>(
ioExecutor);
}
public Payload listen(KeyPair localKeyPair) {
LOG.info("Starting BQP listeners");
// Derive commitment
byte[] commitment = crypto.deriveKeyCommitment(
localKeyPair.getPublic().getEncoded());
// Start all listeners and collect their descriptors
List<TransportDescriptor> descriptors =
new ArrayList<TransportDescriptor>();
for (DuplexPlugin plugin : pluginManager.getKeyAgreementPlugins()) {
KeyAgreementListener l = plugin.createKeyAgreementListener(
commitment);
if (l != null) {
TransportDescriptor d = l.getDescriptor();
descriptors.add(d);
pending.add(connect.submit(new ReadableTask(l.listen())));
listeners.add(l);
}
}
return new Payload(commitment, descriptors);
}
public void stopListening() {
LOG.info("Stopping BQP listeners");
for (KeyAgreementListener l : listeners) {
l.close();
}
listeners.clear();
}
public KeyAgreementTransport connect(Payload remotePayload,
boolean alice) {
// Let the listeners know if we are Alice
this.connecting = true;
this.alice = alice;
long end = clock.currentTimeMillis() + CONNECTION_TIMEOUT;
// Start connecting over supported transports
LOG.info("Starting outgoing BQP connections");
for (TransportDescriptor d : remotePayload.getTransportDescriptors()) {
DuplexPlugin plugin = (DuplexPlugin) pluginManager.getPlugin(
d.getIdentifier());
if (plugin != null)
pending.add(connect.submit(new ReadableTask(
new ConnectorTask(plugin, remotePayload.getCommitment(),
d, end))));
}
// Get chosen connection
KeyAgreementConnection chosen = null;
try {
long now = clock.currentTimeMillis();
Future<KeyAgreementConnection> f =
connect.poll(end - now, MILLISECONDS);
if (f == null)
return null; // No task completed within the timeout.
chosen = f.get();
return new KeyAgreementTransport(chosen);
} catch (InterruptedException e) {
LOG.info("Interrupted while waiting for connection");
Thread.currentThread().interrupt();
return null;
} catch (ExecutionException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
} finally {
stopListening();
// Close all other connections
closePending(chosen);
}
}
private void closePending(KeyAgreementConnection chosen) {
for (Future<KeyAgreementConnection> f : pending) {
try {
if (f.cancel(true))
LOG.info("Cancelled task");
else if (!f.isCancelled()) {
KeyAgreementConnection c = f.get();
if (c != null && c != chosen)
tryToClose(c.getConnection(), false);
}
} catch (InterruptedException e) {
LOG.info("Interrupted while closing sockets");
Thread.currentThread().interrupt();
return;
} catch (ExecutionException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
}
}
private void tryToClose(DuplexTransportConnection conn, boolean exception) {
try {
if (LOG.isLoggable(INFO))
LOG.info("Closing connection, exception: " + exception);
conn.getReader().dispose(exception, true);
conn.getWriter().dispose(exception);
} catch (IOException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
}
private class ConnectorTask implements Callable<KeyAgreementConnection> {
private final byte[] commitment;
private final TransportDescriptor descriptor;
private final long end;
private final DuplexPlugin plugin;
private ConnectorTask(DuplexPlugin plugin, byte[] commitment,
TransportDescriptor descriptor, long end) {
this.plugin = plugin;
this.commitment = commitment;
this.descriptor = descriptor;
this.end = end;
}
@Override
public KeyAgreementConnection call() throws Exception {
// Repeat attempts until we connect or get interrupted
while (true) {
long now = clock.currentTimeMillis();
DuplexTransportConnection conn =
plugin.createKeyAgreementConnection(commitment,
descriptor, end - now);
if (conn != null) {
if (LOG.isLoggable(INFO))
LOG.info(plugin.getId().getString() +
": Outgoing connection");
return new KeyAgreementConnection(conn, plugin.getId());
}
// Wait 2s before retry (to circumvent transient failures)
Thread.sleep(2000);
}
}
}
private class ReadableTask
implements Callable<KeyAgreementConnection> {
private final Callable<KeyAgreementConnection> connectionTask;
private ReadableTask(Callable<KeyAgreementConnection> connectionTask) {
this.connectionTask = connectionTask;
}
@Override
public KeyAgreementConnection call()
throws Exception {
KeyAgreementConnection c = connectionTask.call();
InputStream in = c.getConnection().getReader().getInputStream();
boolean waitingSent = false;
while (!alice && in.available() == 0) {
if (!waitingSent && connecting && !alice) {
// Bob waits here until Alice obtains his payload.
callbacks.connectionWaiting();
waitingSent = true;
}
if (LOG.isLoggable(INFO))
LOG.info(c.getTransportId().toString() +
": Waiting for connection");
Thread.sleep(1000);
}
if (!alice && LOG.isLoggable(INFO))
LOG.info(c.getTransportId().toString() + ": Data available");
return c;
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,157 @@
package org.briarproject.keyagreement;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyPair;
import org.briarproject.api.crypto.SecretKey;
import org.briarproject.api.keyagreement.Payload;
import org.briarproject.api.keyagreement.PayloadEncoder;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
/**
* Implementation of the BQP protocol.
* <p/>
* Alice:
* <ul>
* <li>Send A_KEY</li>
* <li>Receive B_KEY
* <ul>
* <li>Check B_KEY matches B_COMMIT</li>
* </ul></li>
* <li>Calculate s</li>
* <li>Send A_CONFIRM</li>
* <li>Receive B_CONFIRM
* <ul>
* <li>Check B_CONFIRM matches expected</li>
* </ul></li>
* <li>Derive master</li>
* </ul><p/>
* Bob:
* <ul>
* <li>Receive A_KEY
* <ul>
* <li>Check A_KEY matches A_COMMIT</li>
* </ul></li>
* <li>Send B_KEY</li>
* <li>Calculate s</li>
* <li>Receive A_CONFIRM
* <ul>
* <li>Check A_CONFIRM matches expected</li>
* </ul></li>
* <li>Send B_CONFIRM</li>
* <li>Derive master</li>
* </ul>
*/
class KeyAgreementProtocol {
interface Callbacks {
void connectionWaiting();
void initialPacketReceived();
}
private Callbacks callbacks;
private CryptoComponent crypto;
private PayloadEncoder payloadEncoder;
private KeyAgreementTransport transport;
private Payload theirPayload, ourPayload;
private KeyPair ourKeyPair;
private boolean alice;
public KeyAgreementProtocol(Callbacks callbacks, CryptoComponent crypto,
PayloadEncoder payloadEncoder, KeyAgreementTransport transport,
Payload theirPayload, Payload ourPayload, KeyPair ourKeyPair,
boolean alice) {
this.callbacks = callbacks;
this.crypto = crypto;
this.payloadEncoder = payloadEncoder;
this.transport = transport;
this.theirPayload = theirPayload;
this.ourPayload = ourPayload;
this.ourKeyPair = ourKeyPair;
this.alice = alice;
}
/**
* Perform the BQP protocol.
*
* @return the negotiated master secret.
* @throws AbortException when the protocol may have been tampered with.
* @throws IOException for all other other connection errors.
*/
public SecretKey perform() throws AbortException, IOException {
try {
byte[] theirPublicKey;
if (alice) {
sendKey();
// Alice waits here until Bob obtains her payload.
callbacks.connectionWaiting();
theirPublicKey = receiveKey();
} else {
theirPublicKey = receiveKey();
sendKey();
}
SecretKey s = deriveSharedSecret(theirPublicKey);
if (alice) {
sendConfirm(s, theirPublicKey);
receiveConfirm(s, theirPublicKey);
} else {
receiveConfirm(s, theirPublicKey);
sendConfirm(s, theirPublicKey);
}
return crypto.deriveMasterSecret(s);
} catch (AbortException e) {
sendAbort(e.getCause() != null);
throw e;
}
}
private void sendKey() throws IOException {
transport.sendKey(ourKeyPair.getPublic().getEncoded());
}
private byte[] receiveKey() throws AbortException {
byte[] publicKey = transport.receiveKey();
callbacks.initialPacketReceived();
byte[] expected = crypto.deriveKeyCommitment(publicKey);
if (!Arrays.equals(expected, theirPayload.getCommitment()))
throw new AbortException();
return publicKey;
}
private SecretKey deriveSharedSecret(byte[] theirPublicKey)
throws AbortException {
try {
return crypto.deriveSharedSecret(theirPublicKey, ourKeyPair, alice);
} catch (GeneralSecurityException e) {
throw new AbortException(e);
}
}
private void sendConfirm(SecretKey s, byte[] theirPublicKey)
throws IOException {
byte[] confirm = crypto.deriveConfirmationRecord(s,
payloadEncoder.encode(theirPayload),
payloadEncoder.encode(ourPayload),
theirPublicKey, ourKeyPair,
alice, alice);
transport.sendConfirm(confirm);
}
private void receiveConfirm(SecretKey s, byte[] theirPublicKey)
throws AbortException {
byte[] confirm = transport.receiveConfirm();
byte[] expected = crypto.deriveConfirmationRecord(s,
payloadEncoder.encode(theirPayload),
payloadEncoder.encode(ourPayload),
theirPublicKey, ourKeyPair,
alice, !alice);
if (!Arrays.equals(expected, confirm))
throw new AbortException();
}
private void sendAbort(boolean exception) {
transport.sendAbort(exception);
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,68 @@
package org.briarproject.keyagreement;
import org.briarproject.api.FormatException;
import org.briarproject.api.TransportId;
import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.keyagreement.Payload;
import org.briarproject.api.keyagreement.PayloadParser;
import org.briarproject.api.keyagreement.TransportDescriptor;
import org.briarproject.api.properties.TransportProperties;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import static org.briarproject.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION;
import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
class PayloadParserImpl implements PayloadParser {
private final BdfReaderFactory bdfReaderFactory;
@Inject
public PayloadParserImpl(BdfReaderFactory bdfReaderFactory) {
this.bdfReaderFactory = bdfReaderFactory;
}
@Override
public Payload parse(byte[] raw) throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(raw);
BdfReader r = bdfReaderFactory.createReader(in);
r.readListStart(); // Payload start
int proto = (int) r.readLong();
if (proto != PROTOCOL_VERSION)
throw new FormatException();
byte[] commitment = r.readRaw(COMMIT_LENGTH);
if (commitment.length != COMMIT_LENGTH)
throw new FormatException();
List<TransportDescriptor> descriptors = new ArrayList<TransportDescriptor>();
r.readListStart(); // Descriptors start
while (r.hasList()) {
r.readListStart();
while (!r.hasListEnd()) {
TransportId id =
new TransportId(r.readString(MAX_PROPERTY_LENGTH));
TransportProperties p = new TransportProperties();
r.readDictionaryStart();
while (!r.hasDictionaryEnd()) {
String key = r.readString(MAX_PROPERTY_LENGTH);
String value = r.readString(MAX_PROPERTY_LENGTH);
p.put(key, value);
}
r.readDictionaryEnd();
descriptors.add(new TransportDescriptor(id, p));
}
r.readListEnd();
}
r.readListEnd(); // Descriptors end
r.readListEnd(); // Payload end
if (!r.eof())
throw new FormatException();
return new Payload(commitment, descriptors);
}
}

View File

@@ -149,6 +149,13 @@ class PluginManagerImpl implements PluginManager, Service {
return Collections.unmodifiableList(supported);
}
public Collection<DuplexPlugin> getKeyAgreementPlugins() {
List<DuplexPlugin> supported = new ArrayList<DuplexPlugin>();
for (DuplexPlugin d : duplexPlugins)
if (d.supportsKeyAgreement()) supported.add(d);
return Collections.unmodifiableList(supported);
}
private class SimplexPluginStarter implements Runnable {
private final SimplexPluginFactory factory;

View File

@@ -2,6 +2,8 @@ package org.briarproject.plugins.tcp;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.keyagreement.KeyAgreementListener;
import org.briarproject.api.keyagreement.TransportDescriptor;
import org.briarproject.api.plugins.Backoff;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
@@ -250,6 +252,20 @@ abstract class TcpPlugin implements DuplexPlugin {
throw new UnsupportedOperationException();
}
public boolean supportsKeyAgreement() {
return false;
}
public KeyAgreementListener createKeyAgreementListener(
byte[] commitment) {
throw new UnsupportedOperationException();
}
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, TransportDescriptor d, long timeout) {
throw new UnsupportedOperationException();
}
protected Collection<InetAddress> getLocalIpAddresses() {
List<NetworkInterface> ifaces;
try {

View File

@@ -3,6 +3,9 @@ package org.briarproject.plugins.bluetooth;
import org.briarproject.api.TransportId;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.keyagreement.KeyAgreementConnection;
import org.briarproject.api.keyagreement.KeyAgreementListener;
import org.briarproject.api.keyagreement.TransportDescriptor;
import org.briarproject.api.plugins.Backoff;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
@@ -50,6 +53,9 @@ class BluetoothPlugin implements DuplexPlugin {
Logger.getLogger(BluetoothPlugin.class.getName());
private static final int UUID_BYTES = 16;
private static final String PROP_ADDRESS = "address";
private static final String PROP_UUID = "uuid";
private final Executor ioExecutor;
private final SecureRandom secureRandom;
private final Backoff backoff;
@@ -106,7 +112,7 @@ class BluetoothPlugin implements DuplexPlugin {
if (!running) return;
// Advertise the Bluetooth address to contacts
TransportProperties p = new TransportProperties();
p.put("address", localDevice.getBluetoothAddress());
p.put(PROP_ADDRESS, localDevice.getBluetoothAddress());
callback.mergeLocalProperties(p);
// Bind a server socket to accept connections from contacts
String url = makeUrl("localhost", getUuid());
@@ -135,13 +141,13 @@ class BluetoothPlugin implements DuplexPlugin {
}
private String getUuid() {
String uuid = callback.getLocalProperties().get("uuid");
String uuid = callback.getLocalProperties().get(PROP_UUID);
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
TransportProperties p = new TransportProperties();
p.put("uuid", uuid);
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
}
return uuid;
@@ -203,9 +209,9 @@ class BluetoothPlugin implements DuplexPlugin {
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
final ContactId c = e.getKey();
if (connected.contains(c)) continue;
final String address = e.getValue().get("address");
final String address = e.getValue().get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) continue;
final String uuid = e.getValue().get("uuid");
final String uuid = e.getValue().get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) continue;
ioExecutor.execute(new Runnable() {
public void run() {
@@ -236,9 +242,9 @@ class BluetoothPlugin implements DuplexPlugin {
if (!running) return null;
TransportProperties p = callback.getRemoteProperties().get(c);
if (p == null) return null;
String address = p.get("address");
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get("uuid");
String uuid = p.get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) return null;
String url = makeUrl(address, uuid);
StreamConnection s = connect(url);
@@ -335,6 +341,54 @@ class BluetoothPlugin implements DuplexPlugin {
});
}
public boolean supportsKeyAgreement() {
return true;
}
public KeyAgreementListener createKeyAgreementListener(
byte[] localCommitment) {
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(localCommitment).toString();
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
String url = makeUrl("localhost", uuid);
// Make the device discoverable if possible
makeDeviceDiscoverable();
// Bind a server socket for receiving invitation connections
final StreamConnectionNotifier ss;
try {
ss = (StreamConnectionNotifier) Connector.open(url);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
}
if (!running) {
tryToClose(ss);
return null;
}
TransportProperties p = new TransportProperties();
p.put(PROP_ADDRESS, localDevice.getBluetoothAddress());
TransportDescriptor d = new TransportDescriptor(ID, p);
return new BluetoothKeyAgreementListener(d, ss);
}
public DuplexTransportConnection createKeyAgreementConnection(
byte[] remoteCommitment, TransportDescriptor d, long timeout) {
if (!isRunning()) return null;
if (!ID.equals(d.getIdentifier())) return null;
TransportProperties p = d.getProperties();
if (p == null) return null;
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(remoteCommitment).toString();
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
String url = makeUrl(address, uuid);
StreamConnection s = connect(url);
if (s == null) return null;
return new BluetoothTransportConnection(this, s);
}
private void makeDeviceDiscoverable() {
// Try to make the device discoverable (requires root on Linux)
try {
@@ -414,4 +468,39 @@ class BluetoothPlugin implements DuplexPlugin {
return s;
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final StreamConnectionNotifier ss;
public BluetoothKeyAgreementListener(TransportDescriptor descriptor,
StreamConnectionNotifier ss) {
super(descriptor);
this.ss = ss;
}
@Override
public Callable<KeyAgreementConnection> listen() {
return new Callable<KeyAgreementConnection>() {
@Override
public KeyAgreementConnection call() throws Exception {
StreamConnection s = ss.acceptAndOpen();
if (LOG.isLoggable(INFO))
LOG.info(ID.getString() + ": Incoming connection");
return new KeyAgreementConnection(
new BluetoothTransportConnection(
BluetoothPlugin.this, s), ID);
}
};
}
@Override
public void close() {
try {
ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
}
}

View File

@@ -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());

View File

@@ -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',
]

View File

@@ -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();
}
}